UNPKG

@animoca/ethereum-contracts-sale

Version:
621 lines (503 loc) 21.5 kB
const {network, web3} = require('hardhat'); const {BN, balance, ether, expectRevert} = require('@openzeppelin/test-helpers'); const {ZeroAddress, Zero, One, Two, Three, Four} = require('@animoca/ethereum-contracts-core').constants; const {stringToBytes32, bytes32ToUint} = require('../../utils/bytes32'); const {purchasingScenario} = require('../../scenarios'); const Sale = artifacts.require('SwapSaleMock'); const ERC20 = artifacts.require('ERC20Mock'); const skusCapacity = One; const tokensPerSkuCapacity = Four; const sku = stringToBytes32('sku'); const skuTotalSupply = Three; const skuMaxQuantityPerPurchase = Two; const skuNotificationsReceiver = ZeroAddress; const referenceTokenPrice = new BN('1000'); const userData = stringToBytes32('userData'); let owner, payoutWallet, purchaser, recipient; describe('SwapSale', function () { before(async function () { [owner, payoutWallet, purchaser, recipient] = await web3.eth.getAccounts(); }); async function doDeploy(params = {}) { const registry = await artifacts.require('ForwarderRegistry').new(); const forwarder = await artifacts.require('UniversalForwarder').new(); this.referenceToken = await ERC20.new([owner], [params.referenceTokenSupply || ether('1000')], registry.address, forwarder.address, { from: owner, }); this.erc20Token = await ERC20.new([owner], [params.erc20TokenSupply || ether('1000')], registry.address, forwarder.address, {from: owner}); this.contract = await Sale.new( params.payoutWallet || payoutWallet, params.skusCapacity || skusCapacity, params.tokensPerSkuCapacity || tokensPerSkuCapacity, params.referenceToken || this.referenceToken.address, {from: params.owner || owner} ); this.ethTokenAddress = await this.contract.TOKEN_ETH(); } 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.oraclePrice = await this.contract.PRICE_SWAP_VIA_ORACLE(); const skuTokens = [this.referenceToken.address, this.ethTokenAddress, this.erc20Token.address]; const tokenPrices = [ referenceTokenPrice, // reference token this.oraclePrice, // ETH token this.oraclePrice, ]; // ERC20 token return await this.contract.updateSkuPricing(params.sku || sku, params.tokens || skuTokens, params.prices || tokenPrices, { from: params.owner || owner, }); } async function doSetSwapRates(params = {}) { const tokenRates = {}; tokenRates[this.referenceToken.address] = params.referenceTokenRate != undefined ? params.referenceTokenRate : ether('1'); tokenRates[this.ethTokenAddress] = params.ethTokenRate != undefined ? params.ethTokenRate : ether('2'); tokenRates[this.erc20Token.address] = params.erc20Rate != undefined ? params.erc20Rate : ether('0.5'); for (const [token, rate] of Object.entries(tokenRates)) { await this.contract.setMockSwapRate(token, this.referenceToken.address, rate); } } async function doStart(params = {}) { return await this.contract.start({from: params.owner || owner}); } describe('swapRates()', function () { beforeEach(async function () { await doDeploy.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); }); it('should revert if the oracle does not provide a swap rate for one of the pairs', async function () { await expectRevert(this.contract.swapRates([this.ethTokenAddress], referenceTokenPrice, userData), 'Sale: undefined rate'); }); it(`should return the correct swap rates`, async function () { await doSetSwapRates.bind(this)(); const tokens = [this.referenceToken.address, this.ethTokenAddress, this.erc20Token.address]; const actualRates = await this.contract.swapRates(tokens, referenceTokenPrice, userData); const expectedRates = []; for (const token of tokens) { const rate = await this.contract.mockSwapRates(token, this.referenceToken.address); expectedRates.push(rate); } for (var index = 0; index < tokens.length; ++index) { actualRates[index].should.be.bignumber.equal(expectedRates[index]); } }); }); describe('_payment()', function () { function isEthToken(token, overrides = {}) { return token === (overrides.ethTokenAddress || this.ethTokenAddress); } async function getBalance(token, account, overrides = {}) { if (isEthToken.bind(this)(token, overrides)) { return await balance.current(account); } const contract = await ERC20.at(token); return await contract.balanceOf(account); } async function doCallUnderscorePayment(purchase, overrides = {}) { const contract = overrides.contract || this.contract; const result = await contract.callUnderscorePricing(purchase.recipient, purchase.token, purchase.sku, purchase.quantity, purchase.userData, { from: purchase.purchaser, }); const totalPrice = result.totalPrice; const pricingData = result.pricingData; let amount = overrides.amount || totalPrice; let amountVariance = overrides.amountVariance; if (!amountVariance) { amountVariance = Zero; } amount = amount.add(amountVariance); let etherValue; if (isEthToken.bind(this)(purchase.token, overrides)) { etherValue = amount; } else { const erc20Contract = await ERC20.at(purchase.token); await erc20Contract.approve(contract.address, amount, {from: purchase.purchaser}); etherValue = Zero; } const callUnderscorePayment = contract.callUnderscorePayment( purchase.recipient, purchase.token, purchase.sku, purchase.quantity, purchase.userData, totalPrice, pricingData, { from: purchase.purchaser, value: etherValue, } ); return { callUnderscorePayment, totalPrice, }; } async function shouldHandlePayment(purchase, overrides = {}) { const balanceBefore = await getBalance.bind(this)(purchase.token, purchase.purchaser, overrides); const {callUnderscorePayment, totalPrice} = await doCallUnderscorePayment.bind(this)(purchase, overrides); const receipt = await callUnderscorePayment; const contract = overrides.contract || this.contract; if (isEthToken.bind(this)(purchase.token, overrides)) { const tx = await web3.eth.getTransaction(receipt.tx); const transferredValue = new BN(tx.value); if (overrides.amountVariance) { transferredValue.should.be.bignumber.equal(totalPrice.add(overrides.amountVariance)); } else { transferredValue.should.be.bignumber.equal(totalPrice); } } else { const balanceAfter = await getBalance.bind(this)(purchase.token, purchase.purchaser, overrides); const balanceDiff = balanceBefore.sub(balanceAfter); balanceDiff.should.be.bignumber.equal(totalPrice); } } async function shouldRevertAndNotHandlePayment(revertMessage, purchase, overrides = {}) { const {callUnderscorePayment} = await doCallUnderscorePayment.bind(this)(purchase, overrides); if (revertMessage) { await expectRevert(callUnderscorePayment, revertMessage); } else { await expectRevert.unspecified(callUnderscorePayment); } } beforeEach(async function () { await doDeploy.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); await doSetSwapRates.bind(this)(); await doStart.bind(this)(); await this.erc20Token.transfer(purchaser, ether('1')); await this.erc20Token.transfer(recipient, ether('1')); await this.referenceToken.transfer(this.contract.address, ether('1')); this.erc20TokenAddress = this.erc20Token.address; }); describe('when paying with ETH', function () { describe('when the purchaser and the recipient are the same', function () { describe('when the payment amount is insufficient', function () { it('should revert and not handle payment', async function () { await shouldRevertAndNotHandlePayment.bind(this)( 'Sale: insufficient ETH', { purchaser: recipient, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: new BN(-1), } ); }); }); describe('when the payment amount is sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: recipient, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, {} ); }); }); describe('when the payment amount is more than sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: recipient, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: One, } ); }); }); }); describe('when the purchaser and the recipient are different', function () { describe('when the payment amount is insufficient', function () { it('should revert and not handle payment', async function () { const estimate = await this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, One, userData, {from: purchaser}); await shouldRevertAndNotHandlePayment.bind(this)( 'Sale: insufficient ETH', { purchaser: purchaser, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: new BN(-1), } ); }); }); describe('when the payment amount is sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: purchaser, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, {} ); }); }); describe('when the payment amount is more than sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: purchaser, recipient: recipient, token: this.ethTokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: One, } ); }); }); }); }); describe('when paying with ERC20', function () { describe('when the purchaser and the recipient are the same', function () { describe('when the payment amount is insufficient', function () { it('should revert and not handle payment', async function () { await shouldRevertAndNotHandlePayment.bind(this)( 'ERC20: insufficient allowance', { purchaser: recipient, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: new BN(-1), } ); }); }); describe('when the payment amount is sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: recipient, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, {} ); }); }); describe('when the payment amount is more than sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: recipient, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: One, } ); }); }); }); describe('when the purchaser and the recipient are different', function () { describe('when the payment amount is insufficient', function () { it('should revert and not handle payment', async function () { await shouldRevertAndNotHandlePayment.bind(this)( 'ERC20: insufficient allowance', { purchaser: purchaser, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: new BN(-1), } ); }); }); describe('when the payment amount is sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: purchaser, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, {} ); }); }); describe('when the payment amount is more than sufficient', function () { it('should handle payment', async function () { await shouldHandlePayment.bind(this)( { purchaser: purchaser, recipient: recipient, token: this.erc20TokenAddress, sku: sku, quantity: One, userData: userData, }, { amountVariance: One, } ); }); }); }); }); }); describe('_oraclePricing()', function () { beforeEach(async function () { await doDeploy.bind(this)(); await doCreateSku.bind(this)(); }); it('should revert if the SKU does not exist', async function () { const otherSku = stringToBytes32('other sku'); await expectRevert(this.contract.callUnderscorePricing(ZeroAddress, this.erc20Token.address, otherSku, One, userData), 'Sale: unsupported SKU'); }); it('should revert if the payment token is not supported by the SKU', async function () { const registry = await artifacts.require('ForwarderRegistry').new(); const forwarder = await artifacts.require('UniversalForwarder').new(); const otherToken = await ERC20.new([owner], [ether('1000')], registry.address, forwarder.address, {from: owner}); await expectRevert(this.contract.callUnderscorePricing(ZeroAddress, otherToken.address, sku, One, userData), 'EnumMap: nonexistent key'); }); it('should not handle a fixed total price', async function () { const ethTokenUnitFixedPrice = ether('3'); await doUpdateSkuPricing.bind(this)({ prices: [ One, // reference token ethTokenUnitFixedPrice, // ETH token One, ], // ERC20 token }); const result = await this.contract.callUnderscoreOraclePricing(ZeroAddress, this.ethTokenAddress, sku, One, userData); result.handled.should.be.false; const actualTotalPrice = result.totalPrice; actualTotalPrice.should.be.bignumber.equal(Zero); const pricingData = result.pricingData; pricingData.length.should.be.equal(0); }); it('should set the oracle total price (0 < rate < 1)', async function () { await doUpdateSkuPricing.bind(this)(); await doSetSwapRates.bind(this)(); const swapRate = await this.contract.mockSwapRates(this.erc20Token.address, this.referenceToken.address); const result = await this.contract.callUnderscoreOraclePricing(ZeroAddress, this.erc20Token.address, sku, One, userData); result.handled.should.be.true; const actualTotalPrice = result.totalPrice; const expectedTotalPrice = referenceTokenPrice.mul(new BN(10).pow(new BN(18))).div(swapRate); actualTotalPrice.should.be.bignumber.equal(expectedTotalPrice); const pricingData = result.pricingData; pricingData.length.should.be.equal(2); const unitPrice = bytes32ToUint(pricingData[0]); unitPrice.should.be.bignumber.equal(this.oraclePrice); const actualSwapRate = bytes32ToUint(pricingData[1]); const expectedSwapRate = swapRate; actualSwapRate.should.be.bignumber.equal(expectedSwapRate); }); it('should set the oracle total price (1 == rate)', async function () { await doUpdateSkuPricing.bind(this)(); await doSetSwapRates.bind(this)(); const registry = await artifacts.require('ForwarderRegistry').new(); const forwarder = await artifacts.require('UniversalForwarder').new(); const token = await ERC20.new([owner], [ether('1000')], registry.address, forwarder.address, {from: owner}); await this.contract.updateSkuPricing(sku, [token.address], [this.oraclePrice], {from: owner}); await this.contract.setMockSwapRate(token.address, this.referenceToken.address, ether('1')); const swapRate = await this.contract.mockSwapRates(token.address, this.referenceToken.address); const result = await this.contract.callUnderscoreOraclePricing(ZeroAddress, token.address, sku, One, userData); result.handled.should.be.true; const actualTotalPrice = result.totalPrice; const expectedTotalPrice = referenceTokenPrice.mul(new BN(10).pow(new BN(18))).div(swapRate); actualTotalPrice.should.be.bignumber.equal(expectedTotalPrice); const pricingData = result.pricingData; pricingData.length.should.be.equal(2); const unitPrice = bytes32ToUint(pricingData[0]); unitPrice.should.be.bignumber.equal(this.oraclePrice); const actualSwapRate = bytes32ToUint(pricingData[1]); const expectedSwapRate = swapRate; actualSwapRate.should.be.bignumber.equal(expectedSwapRate); }); it('should set the oracle total price (1 < rate)', async function () { await doUpdateSkuPricing.bind(this)(); await doSetSwapRates.bind(this)(); const swapRate = await this.contract.mockSwapRates(this.ethTokenAddress, this.referenceToken.address); const result = await this.contract.callUnderscoreOraclePricing(ZeroAddress, this.ethTokenAddress, sku, One, userData); result.handled.should.be.true; const actualTotalPrice = result.totalPrice; const expectedTotalPrice = referenceTokenPrice.mul(new BN(10).pow(new BN(18))).div(swapRate); actualTotalPrice.should.be.bignumber.equal(expectedTotalPrice); const pricingData = result.pricingData; pricingData.length.should.be.equal(2); const unitPrice = bytes32ToUint(pricingData[0]); unitPrice.should.be.bignumber.equal(this.oraclePrice); const actualSwapRate = bytes32ToUint(pricingData[1]); const expectedSwapRate = swapRate; actualSwapRate.should.be.bignumber.equal(expectedSwapRate); }); }); describe('scenarios', function () { beforeEach(async function () { await doDeploy.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); await doSetSwapRates.bind(this)(); await doStart.bind(this)(); }); describe('purchasing', function () { beforeEach(async function () { await this.erc20Token.transfer(purchaser, ether('1')); await this.erc20Token.transfer(recipient, ether('1')); await this.referenceToken.transfer(this.contract.address, ether('1')); this.erc20TokenAddress = this.erc20Token.address; this.purchaser = purchaser; this.recipient = recipient; }); purchasingScenario(sku); }); }); });