UNPKG

@animoca/ethereum-contracts-sale

Version:
341 lines (272 loc) 11.5 kB
const {artifacts, web3} = require('hardhat'); const {BN, ether, time, expectRevert} = require('@openzeppelin/test-helpers'); const {ZeroAddress, Zero, One, Two, Three, Four} = require('@animoca/ethereum-contracts-core').constants; const {stringToBytes32} = require('../utils/bytes32'); const Fixture = require('@animoca/ethereum-contracts-core/test/utils/fixture'); const UniswapV2Fixture = require('../fixtures/uniswapv2.fixture'); const {purchasingScenario} = require('../scenarios'); const WETH9 = artifacts.require('WETH9'); const Sale = artifacts.require('UniswapOracleSaleMock'); const ERC20 = artifacts.require('ERC20Mock'); const skusCapacity = One; const tokensPerSkuCapacity = Four; const sku = stringToBytes32('sku'); const skuTotalSupply = Three; const skuMaxQuantityPerPurchase = Two; const skuNotificationsReceiver = ZeroAddress; let owner, payoutWallet, purchaser, recipient; describe('UniswapOracleSale', function () { let loadFixture; before(async function () { let accounts = await web3.eth.getAccounts(); loadFixture = Fixture.createFixtureLoader(accounts, web3.eth.currentProvider); [owner, payoutWallet, purchaser, recipient] = accounts; UniswapV2Fixture.reset(); }); // uniswapv2 fixture adds `contract` field to each token when it's loaded const tokens = { WETH: { abstraction: WETH9, supply: ether('1000'), }, ReferenceToken: { abstraction: ERC20, supply: ether('1000'), }, TokenA: { abstraction: ERC20, supply: ether('1000'), }, TokenB: { abstraction: ERC20, supply: ether('1000'), }, TokenC: { abstraction: ERC20, supply: ether('1000'), }, TokenD: { abstraction: ERC20, supply: ether('1000'), }, }; const tokenPairs = { Pair1: ['WETH', 'ReferenceToken'], Pair2: ['TokenA', 'ReferenceToken'], Pair3: ['TokenB', 'ReferenceToken'], Pair4: ['TokenC', 'ReferenceToken'], }; const liquidity = { ReferenceToken: { amount: new BN('1000000'), price: new BN('1000'), }, TokenA: { amount: new BN('2000'), price: new BN('2'), }, TokenB: { amount: new BN('3000000000'), price: new BN('3000000'), }, TokenC: { amount: new BN('1000000'), price: new BN('1000'), }, }; async function doLoadFixture(params = {}) { const fixture = UniswapV2Fixture.get(params.tokens || tokens, params.tokenPairs || tokenPairs); this.fixtureData = await loadFixture(fixture); } async function doAddLiquidity(params = {}) { const timestamp = await time.latest(); const deadline = params.deadline || timestamp.add(time.duration.minutes(5)); const amountRefTokenMin = params.amountRefTokenMin || Zero; const amountTokenMin = params.amountTokenMin || Zero; const amountEthMin = params.amountEthMin || Zero; const router = this.fixtureData.router; const refTokenKey = 'ReferenceToken'; const refTokenContract = tokens[refTokenKey].contract; const refLiquidityData = liquidity[refTokenKey]; for (const [tokenPairKey, tokenPair] of Object.entries(tokenPairs)) { if (tokenPair.length !== 2) { throw new Error(`Invalid token pair length: ${tokenPairKey}`); } const refTokenIndex = tokenPair.indexOf(refTokenKey); if (refTokenIndex === -1) { throw new Error(`Missing expected reference token in pair: ${tokenPairKey}`); } const tokenIndex = refTokenIndex === 0 ? 1 : 0; const tokenKey = tokenPair[tokenIndex]; await refTokenContract.approve(router.address, refLiquidityData.amount, {from: refLiquidityData.owner || owner}); if (tokenKey === 'WETH') { await router.addLiquidityETH( refTokenContract.address, refLiquidityData.amount, amountRefTokenMin, amountEthMin, refLiquidityData.lpTokenRecipient || owner, deadline, { from: refLiquidityData.owner || owner, value: refLiquidityData.amount.mul(refLiquidityData.price), } ); } else { const tokenContract = tokens[tokenKey].contract; const liquidityData = liquidity[tokenKey]; await tokenContract.approve(router.address, liquidityData.amount, {from: liquidityData.owner || owner}); await router.addLiquidity( tokenContract.address, refTokenContract.address, liquidityData.amount, refLiquidityData.amount, amountTokenMin, amountRefTokenMin, liquidityData.lpTokenRecipient || owner, deadline, { from: liquidityData.owner || owner, } ); await tokenContract.approve(router.address, Zero, {from: liquidityData.owner || owner}); } await refTokenContract.approve(router.address, Zero, {from: refLiquidityData.owner || owner}); } } async function doDeploy(params = {}) { this.contract = await Sale.new( params.payoutWallet || payoutWallet, params.skusCapacity || skusCapacity, params.tokensPerSkuCapacity || tokensPerSkuCapacity, params.referenceToken || tokens['ReferenceToken'].contract.address, params.router || this.fixtureData.router.address, {from: params.owner || owner} ); } async function doCreateSku(params = {}) { return await this.contract.createSku( params.sku || sku, params.skuTotalSupply || skuTotalSupply, params.skuMaxQuantityPerPurchase || skuMaxQuantityPerPurchase, params.skuNotificationsReceiver || skuNotificationsReceiver, {from: params.owner || owner} ); } async function doUpdateSkuPricing(params = {}) { this.ethTokenAddress = await this.contract.TOKEN_ETH(); this.oraclePrice = await this.contract.PRICE_CONVERT_VIA_ORACLE(); const skuTokens = [ tokens['ReferenceToken'].contract.address, this.ethTokenAddress, tokens['TokenA'].contract.address, tokens['TokenB'].contract.address, ]; const tokenPrices = [ liquidity['ReferenceToken'].price, // reference token this.oraclePrice, // ETH this.oraclePrice, // Token A this.oraclePrice, ]; // Token B return await this.contract.updateSkuPricing(params.sku || sku, params.tokens || skuTokens, params.prices || tokenPrices, { from: params.owner || owner, }); } async function doStart(params = {}) { return await this.contract.start({from: params.owner || owner}); } beforeEach(async function () { await doLoadFixture.bind(this)(); }); describe('_conversionRate()', function () { const userData = stringToBytes32('userData'); beforeEach(async function () { await doAddLiquidity.bind(this)(); await doDeploy.bind(this)(); }); it('should revert if the source token to convert from is the zero address', async function () { await expectRevert( this.contract.callUnderscoreConversionRate(ZeroAddress, tokens['TokenA'].contract.address, userData), 'UniswapV2: zero address' ); }); it('should revert if the destination token to convert to is the zero address', async function () { await expectRevert( this.contract.callUnderscoreConversionRate(tokens['TokenA'].contract.address, ZeroAddress, userData), 'UniswapV2: zero address' ); }); it('should revert if the source and destination token are the same', async function () { await expectRevert( this.contract.callUnderscoreConversionRate(tokens['TokenA'].contract.address, tokens['TokenA'].contract.address, userData), 'UniswapV2: same addresses' ); }); it('should revert if the source token to convert from does not belong to a token pair', async function () { await expectRevert( this.contract.callUnderscoreConversionRate(tokens['TokenD'].contract.address, tokens['TokenA'].contract.address, userData), 'revert' ); }); it('should revert if the destination token to convert to does not belong to a token pair', async function () { await expectRevert( this.contract.callUnderscoreConversionRate(tokens['TokenA'].contract.address, tokens['TokenD'].contract.address, userData), 'revert' ); }); describe(`should return the correct conversion rates`, function () { it('when the source token has a reserve less than the destination token', async function () { const fromToken = tokens['TokenA'].contract.address; const toToken = tokens['ReferenceToken'].contract.address; const actualRate = await this.contract.callUnderscoreConversionRate(fromToken, toToken, userData); const reserves = await this.contract.getReserves(fromToken, toToken); const expectedRate = reserves.reserveB.mul(new BN(10).pow(new BN(18))).div(reserves.reserveA); actualRate.should.be.bignumber.equal(expectedRate); }); it('when the source token has a reserve more than the destination token', async function () { const fromToken = tokens['TokenB'].contract.address; const toToken = tokens['ReferenceToken'].contract.address; const actualRate = await this.contract.callUnderscoreConversionRate(fromToken, toToken, userData); const reserves = await this.contract.getReserves(fromToken, toToken); const expectedRate = reserves.reserveB.mul(new BN(10).pow(new BN(18))).div(reserves.reserveA); actualRate.should.be.bignumber.equal(expectedRate); }); it('when the source token has a reserve equal to the destination token', async function () { const fromToken = tokens['TokenC'].contract.address; const toToken = tokens['ReferenceToken'].contract.address; const actualRate = await this.contract.callUnderscoreConversionRate(fromToken, toToken, userData); const reserves = await this.contract.getReserves(fromToken, toToken); const expectedRate = reserves.reserveB.mul(new BN(10).pow(new BN(18))).div(reserves.reserveA); actualRate.should.be.bignumber.equal(expectedRate); }); it('when the source token is the ETH token', async function () { const fromToken = await this.contract.TOKEN_ETH(); const toToken = tokens['ReferenceToken'].contract.address; const actualRate = await this.contract.callUnderscoreConversionRate(fromToken, toToken, userData); const reserves = await this.contract.getReserves(fromToken, toToken); const expectedRate = reserves.reserveB.mul(new BN(10).pow(new BN(18))).div(reserves.reserveA); actualRate.should.be.bignumber.equal(expectedRate); }); }); }); describe('scenarios', function () { beforeEach(async function () { await doAddLiquidity.bind(this)(); await doDeploy.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); await doStart.bind(this)(); }); describe('purchasing', function () { beforeEach(async function () { this.erc20TokenAddress = tokens['TokenA'].contract.address; const erc20TokenContract = await ERC20.at(this.erc20TokenAddress); await erc20TokenContract.transfer(purchaser, ether('1')); await erc20TokenContract.transfer(recipient, ether('1')); this.purchaser = purchaser; this.recipient = recipient; }); purchasingScenario(sku); }); }); });