diff --git a/contracts/zero-ex/test/protocol_fees_test.ts b/contracts/zero-ex/test/protocol_fees_test.ts index 320d3ad8b1..1b2582dd93 100644 --- a/contracts/zero-ex/test/protocol_fees_test.ts +++ b/contracts/zero-ex/test/protocol_fees_test.ts @@ -1,17 +1,19 @@ import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { AuthorizableRevertErrors, BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { artifacts } from './artifacts'; -import { TestProtocolFeesContract, TestStakingContract, TestWethContract } from './wrappers'; +import { FeeCollectorContract, TestProtocolFeesContract, TestStakingContract, TestWethContract } from './wrappers'; blockchainTests.resets('ProtocolFees', env => { let payer: string; + let unauthorized: string; let protocolFees: TestProtocolFeesContract; let staking: TestStakingContract; let weth: TestWethContract; before(async () => { - [payer] = await env.getAccountAddressesAsync(); + [payer, unauthorized] = await env.getAccountAddressesAsync(); protocolFees = await TestProtocolFeesContract.deployFrom0xArtifactAsync( artifacts.TestProtocolFees, env.provider, @@ -35,41 +37,128 @@ blockchainTests.resets('ProtocolFees', env => { await weth.approve(protocolFees.address, constants.ONE_ETHER).awaitTransactionSuccessAsync({ from: payer }); }); + async function collectAsync( + poolId: string, + amount: BigNumber, + etherValue: BigNumber, + ): Promise { + return protocolFees + .collectProtocolFee(poolId, amount, weth.address) + .awaitTransactionSuccessAsync({ from: payer, value: etherValue }); + } + + async function transferFeesAsync(poolId: string): Promise { + return protocolFees.transferFeesForPool(poolId, staking.address, weth.address).awaitTransactionSuccessAsync(); + } + + describe('FeeCollector', () => { + it('should disallow unauthorized initialization', async () => { + const pool = hexUtils.random(); + + await collectAsync(pool, constants.ONE_ETHER, constants.ZERO_AMOUNT); + await transferFeesAsync(pool); + + const feeCollector = new FeeCollectorContract( + await protocolFees.getFeeCollector(pool).callAsync(), + env.provider, + env.txDefaults, + ); + + const tx = feeCollector + .initialize(weth.address, staking.address, pool) + .sendTransactionAsync({ from: unauthorized }); + return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(unauthorized)); + }); + }); + describe('_collectProtocolFee()', () => { - it('can collect a protocol fee multiple times', async () => { - const poolId = hexUtils.random(); - const amount1 = new BigNumber(123456); - const amount2 = new BigNumber(456789); + const pool1 = hexUtils.random(); + const pool2 = hexUtils.random(); - // Transfer amount1 via WETH. - await protocolFees - .collectProtocolFee(poolId, amount1, weth.address) - .awaitTransactionSuccessAsync({ from: payer }); + it('should revert if WETH transfer fails', async () => { + const tooMuch = constants.ONE_ETHER.plus(1); + const tx = collectAsync(pool1, constants.ONE_ETHER.plus(1), constants.ZERO_AMOUNT); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Spender.SpenderERC20TransferFromFailedError( + weth.address, + payer, + undefined, + tooMuch, + undefined, + ), + ); + }); - // Send to staking contract. - await protocolFees - .transferFeesForPool(poolId, staking.address, weth.address) - .awaitTransactionSuccessAsync(); + it('should revert if insufficient ETH transferred', async () => { + const tooLittle = constants.ONE_ETHER.minus(1); + const tx = collectAsync(pool1, constants.ONE_ETHER, tooLittle); + return expect(tx).to.revertWith('FixinProtocolFees/ETHER_TRANSFER_FALIED'); + }); - // Transfer amount2 via ETH. - await protocolFees - .collectProtocolFee(poolId, amount2, weth.address) - .awaitTransactionSuccessAsync({ from: payer, value: amount2 }); + it('should accept WETH fee', async () => { + const beforeWETH = await weth.balanceOf(payer).callAsync(); + await collectAsync(pool1, constants.ONE_ETHER, constants.ZERO_AMOUNT); + const afterWETH = await weth.balanceOf(payer).callAsync(); - // Send to staking contract again. - await protocolFees - .transferFeesForPool(poolId, staking.address, weth.address) - .awaitTransactionSuccessAsync(); + return expect(beforeWETH.minus(afterWETH)).to.bignumber.eq(constants.ONE_ETHER); + }); - const balance = await staking.balanceForPool(poolId).callAsync(); - const wethBalance = await weth.balanceOf(staking.address).callAsync(); + it('should accept ETH fee', async () => { + const beforeWETH = await weth.balanceOf(payer).callAsync(); + const beforeETH = await env.web3Wrapper.getBalanceInWeiAsync(payer); + await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); + const afterWETH = await weth.balanceOf(payer).callAsync(); + const afterETH = await env.web3Wrapper.getBalanceInWeiAsync(payer); - // Check that staking accounted for the collected ether properly. - expect(balance).to.bignumber.eq(wethBalance); + // We check for greater than 1 ether spent to allow for spending on gas. + await expect(beforeETH.minus(afterETH)).to.bignumber.gt(constants.ONE_ETHER); + return expect(beforeWETH).to.bignumber.eq(afterWETH); + }); - // We leave 1 wei behind, of both ETH and WETH, for gas reasons. - const total = amount1.plus(amount2).minus(2); - return expect(balance).to.bignumber.eq(total); + it('should transfer both ETH and WETH', async () => { + await collectAsync(pool1, constants.ONE_ETHER, constants.ZERO_AMOUNT); + await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); + await transferFeesAsync(pool1); + + const balanceWETH = await weth.balanceOf(staking.address).callAsync(); + + // We leave 1 wei behind of both ETH and WETH. + return expect(balanceWETH).to.bignumber.eq(constants.ONE_ETHER.times(2).minus(2)); + }); + + it('should accept ETH after first transfer', async () => { + await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); + await transferFeesAsync(pool1); + await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); + await transferFeesAsync(pool1); + + const balanceWETH = await weth.balanceOf(staking.address).callAsync(); + + // We leave 1 wei behind of both ETH and WETH + return expect(balanceWETH).to.bignumber.eq(constants.ONE_ETHER.times(2).minus(2)); + }); + + it('should attribute fees correctly', async () => { + const pool1Amount = new BigNumber(12345); + const pool2Amount = new BigNumber(45678); + + await collectAsync(pool1, pool1Amount, pool1Amount); // ETH + await transferFeesAsync(pool1); + await collectAsync(pool2, pool2Amount, constants.ZERO_AMOUNT); // WETH + await transferFeesAsync(pool2); + + const pool1Balance = await staking.balanceForPool(pool1).callAsync(); + const pool2Balance = await staking.balanceForPool(pool2).callAsync(); + + const balanceWETH = await weth.balanceOf(staking.address).callAsync(); + + await expect(balanceWETH).to.bignumber.equal(pool1Balance.plus(pool2Balance)); + + // We leave 1 wei behind of both ETH and WETH. + await expect(pool1Balance).to.bignumber.equal(pool1Amount.minus(2)); + + // Here we paid in WETH, so there's just 1 wei of WETH held back. + return expect(pool2Balance).to.bignumber.equal(pool2Amount.minus(1)); }); }); });