diff options
Diffstat (limited to 'packages/contracts/test')
62 files changed, 12138 insertions, 2480 deletions
diff --git a/packages/contracts/test/asset_proxy/authorizable.ts b/packages/contracts/test/asset_proxy/authorizable.ts new file mode 100644 index 000000000..e99c6cee3 --- /dev/null +++ b/packages/contracts/test/asset_proxy/authorizable.ts @@ -0,0 +1,207 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { MixinAuthorizableContract } from '../../generated_contract_wrappers/mixin_authorizable'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Authorizable', () => { + let owner: string; + let notOwner: string; + let address: string; + let authorizable: MixinAuthorizableContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = address = accounts[0]; + notOwner = accounts[1]; + authorizable = await MixinAuthorizableContract.deployFrom0xArtifactAsync( + artifacts.MixinAuthorizable, + provider, + txDefaults, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('addAuthorizedAddress', () => { + it('should throw if not called by owner', async () => { + return expectTransactionFailedAsync( + authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }), + RevertReason.OnlyContractOwner, + ); + }); + it('should allow owner to add an authorized address', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.true(); + }); + it('should throw if owner attempts to authorize a duplicate address', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return expectTransactionFailedAsync( + authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + RevertReason.TargetAlreadyAuthorized, + ); + }); + }); + + describe('removeAuthorizedAddress', () => { + it('should throw if not called by owner', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { + from: notOwner, + }), + RevertReason.OnlyContractOwner, + ); + }); + + it('should allow owner to remove an authorized address', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.false(); + }); + + it('should throw if owner attempts to remove an address that is not authorized', async () => { + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { + from: owner, + }), + RevertReason.TargetNotAuthorized, + ); + }); + }); + + describe('removeAuthorizedAddressAtIndex', () => { + it('should throw if not called by owner', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(0); + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: notOwner, + }), + RevertReason.OnlyContractOwner, + ); + }); + it('should throw if index is >= authorities.length', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(1); + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: owner, + }), + RevertReason.IndexOutOfBounds, + ); + }); + it('should throw if owner attempts to remove an address that is not authorized', async () => { + const index = new BigNumber(0); + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: owner, + }), + RevertReason.TargetNotAuthorized, + ); + }); + it('should throw if address at index does not match target', async () => { + const address1 = address; + const address2 = notOwner; + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address1, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address2, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const address1Index = new BigNumber(0); + return expectTransactionFailedAsync( + authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address2, address1Index, { + from: owner, + }), + RevertReason.AuthorizedAddressMismatch, + ); + }); + it('should allow owner to remove an authorized address', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(0); + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.false(); + }); + }); + + describe('getAuthorizedAddresses', () => { + it('should return all authorized addresses', async () => { + const initial = await authorizable.getAuthorizedAddresses.callAsync(); + expect(initial).to.have.length(0); + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const afterAdd = await authorizable.getAuthorizedAddresses.callAsync(); + expect(afterAdd).to.have.length(1); + expect(afterAdd).to.include(address); + + await web3Wrapper.awaitTransactionSuccessAsync( + await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const afterRemove = await authorizable.getAuthorizedAddresses.callAsync(); + expect(afterRemove).to.have.length(0); + }); + }); +}); diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts new file mode 100644 index 000000000..62215f08d --- /dev/null +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -0,0 +1,386 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721ReceiverContract } from '../../generated_contract_wrappers/dummy_erc721_receiver'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { IAssetProxyContract } from '../../generated_contract_wrappers/i_asset_proxy'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const assetProxyInterface = new IAssetProxyContract( + artifacts.IAssetProxy.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); + +// tslint:disable:no-unnecessary-type-assertion +describe('Asset Transfer Proxies', () => { + let owner: string; + let notAuthorized: string; + let exchangeAddress: string; + let makerAddress: string; + let takerAddress: string; + + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let erc721Receiver: DummyERC721ReceiverContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let erc721MakerTokenId: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice( + accounts, + 0, + 5, + )); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 1; + [zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerTokenId = erc721Balances[makerAddress][erc721Token.address][0]; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Receiver, + provider, + txDefaults, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('Transfer Proxy - ERC20', () => { + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(amount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].add(amount), + ); + }); + + it('should do nothing if transferring 0 amount of a token', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address], + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address], + ); + }); + + it('should throw if allowances are too low', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Create allowance less than transfer amount. Set allowance on proxy. + const allowance = new BigNumber(0); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Perform a transfer; expect this to fail. + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.TransferFailed, + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); + + it('should have an id of 0xf47261b0', async () => { + const proxyId = await erc20Proxy.getProxyId.callAsync(); + const expectedProxyId = '0xf47261b0'; + expect(proxyId).to.equal(expectedProxyId); + }); + }); + + describe('Transfer Proxy - ERC721', () => { + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + }); + + it('should not call onERC721Received when transferring to a smart contract', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + erc721Receiver.address, + amount, + ); + const logDecoder = new LogDecoder(web3Wrapper, erc721Receiver.address); + const tx = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + ); + // Verify that no log was emitted by erc721 receiver + expect(tx.logs.length).to.be.equal(0); + // Verify transfer was successful + const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); + }); + + it('should throw if transferring 0 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw if transferring > 1 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(500); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw if allowances are too low', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Remove transfer approval for makerAddress. + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721MakerTokenId, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Perform a transfer; expect this to fail. + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.TransferFailed, + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); + + it('should have an id of 0x02571792', async () => { + const proxyId = await erc721Proxy.getProxyId.callAsync(); + const expectedProxyId = '0x02571792'; + expect(proxyId).to.equal(expectedProxyId); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion +// tslint:disable:max-file-line-count diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index e276d11b3..b324988cc 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -1,124 +1,125 @@ -import { LogWithDecodedArgs, SignedOrder, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; import ethUtil = require('ethereumjs-util'); -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { crypto } from '../../util/crypto'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('Exchange', () => { - let maker: string; - let tokenOwner: string; - let taker: string; - let feeRecipient: string; - const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - - let rep: DummyTokenContract; - let dgd: DummyTokenContract; - let zrx: DummyTokenContract; +// tslint:disable:no-unnecessary-type-assertion +describe('Exchange core', () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; let exchange: ExchangeContract; - let tokenTransferProxy: TokenTransferProxyContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; let signedOrder: SignedOrder; - let balances: BalancesByOwner; - let exWrapper: ExchangeWrapper; - let dmyBalances: Balances; + let erc20Balances: ERC20BalancesByOwner; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; let orderFactory: OrderFactory; - let zeroEx: ZeroEx; + let erc721MakerAssetIds: BigNumber[]; + let erc721TakerAssetIds: BigNumber[]; + + let defaultMakerAssetAddress: string; + let defaultTakerAssetAddress: string; before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - maker = accounts[0]; - [tokenOwner, taker, feeRecipient] = accounts; - const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - dgd = new DummyTokenContract(web3Wrapper, dgdInstance.abi, dgdInstance.address); - zrx = new DummyTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4)); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, ); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); - zeroEx = new ZeroEx(web3.currentProvider, { - exchangeContractAddress: exchange.address, - networkId: constants.TESTRPC_NETWORK_ID, - }); - exWrapper = new ExchangeWrapper(exchange, zeroEx); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(zrxToken.address), + ); + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - dmyBalances = new Balances([rep, dgd, zrx], [maker, taker, feeRecipient]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }), - rep.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - rep.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, }), - dgd.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - dgd.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, }), - zrx.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - zrx.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - ]); + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -126,756 +127,454 @@ describe('Exchange', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('internal functions', () => { - it('should include transferViaTokenTransferProxy', () => { - expect((exchange as any).transferViaTokenTransferProxy).to.be.undefined(); - }); - - it('should include isTransferable', () => { - expect((exchange as any).isTransferable).to.be.undefined(); - }); - - it('should include getBalance', () => { - expect((exchange as any).getBalance).to.be.undefined(); - }); - - it('should include getAllowance', () => { - expect((exchange as any).getAllowance).to.be.undefined(); - }); - }); - describe('fillOrder', () => { beforeEach(async () => { - balances = await dmyBalances.getAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); signedOrder = await orderFactory.newSignedOrderAsync(); }); - it('should create an unfillable order', async () => { + it('should throw if signature is invalid', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(1001), - takerTokenAmount: new BigNumber(3), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount1 = new BigNumber(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: fillTakerTokenAmount1, - }); - - const filledTakerTokenAmountAfter1 = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter1).to.be.bignumber.equal(fillTakerTokenAmount1); - - const fillTakerTokenAmount2 = new BigNumber(1); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: fillTakerTokenAmount2, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), }); - const filledTakerTokenAmountAfter2 = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), + const v = ethUtil.toBuffer(signedOrder.signature.slice(0, 4)); + const invalidR = ethUtil.sha3('invalidR'); + const invalidS = ethUtil.sha3('invalidS'); + const signatureType = ethUtil.toBuffer(`0x${signedOrder.signature.slice(-2)}`); + const invalidSigBuff = Buffer.concat([v, invalidR, invalidS, signatureType]); + const invalidSigHex = `0x${invalidSigBuff.toString('hex')}`; + signedOrder.signature = invalidSigHex; + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress), + RevertReason.InvalidOrderSignature, ); - expect(filledTakerTokenAmountAfter2).to.be.bignumber.equal(filledTakerTokenAmountAfter1); }); - it('should transfer the correct amounts when makerTokenAmount === takerTokenAmount', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + it('should throw if no value is filled', async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress), + RevertReason.OrderUnfillable, ); }); + }); - it('should transfer the correct amounts when makerTokenAmount > takerTokenAmount', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); + describe('cancelOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + signedOrder = await orderFactory.newSignedOrderAsync(); + }); - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + it('should throw if not sent by maker', async () => { + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, takerAddress), + RevertReason.InvalidMaker, ); }); - it('should transfer the correct amounts when makerTokenAmount < takerTokenAmount', async () => { + it('should throw if makerAssetAmount is 0', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + makerAssetAmount: new BigNumber(0), }); - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, ); }); - it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => { + it('should throw if takerAssetAmount is 0', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - taker, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + takerAssetAmount: new BigNumber(0), }); - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - const expectedFillAmountTAfter = fillTakerTokenAmount.add(filledTakerTokenAmountBefore); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(expectedFillAmountTAfter); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, ); }); - it('should fill remaining value if fillTakerTokenAmount > remaining takerTokenAmount', async () => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount, - }); - const log = res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>; - expect(log.args.filledTakerTokenAmount).to.be.bignumber.equal( - signedOrder.takerTokenAmount.minus(fillTakerTokenAmount), - ); - const newBalances = await dmyBalances.getAsync(); - - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(signedOrder.makerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(signedOrder.takerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(signedOrder.makerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(signedOrder.takerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(signedOrder.makerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(signedOrder.takerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(signedOrder.makerFee.add(signedOrder.takerFee)), + it('should be able to cancel a full order', async () => { + await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: signedOrder.takerAssetAmount.div(2), + }), + RevertReason.OrderUnfillable, ); }); - it('should log 1 event with the correct arguments when order has a feeRecipient', async () => { - const divisor = 2; - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), - }); - expect(res.logs).to.have.length(1); - - const logArgs = (res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>).args; - const expectedFilledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedFilledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const expectedFeeMPaid = signedOrder.makerFee.div(divisor); - const expectedFeeTPaid = signedOrder.takerFee.div(divisor); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(taker).to.be.equal(logArgs.taker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedFilledMakerTokenAmount).to.be.bignumber.equal(logArgs.filledMakerTokenAmount); - expect(expectedFilledTakerTokenAmount).to.be.bignumber.equal(logArgs.filledTakerTokenAmount); - expect(expectedFeeMPaid).to.be.bignumber.equal(logArgs.paidMakerFee); - expect(expectedFeeTPaid).to.be.bignumber.equal(logArgs.paidTakerFee); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); - }); - - it('should log 1 event with the correct arguments when order has no feeRecipient', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - feeRecipient: ZeroEx.NULL_ADDRESS, - }); - const divisor = 2; - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), - }); + it('should log 1 event with correct arguments', async () => { + const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const logArgs = (res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>).args; - const expectedFilledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedFilledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const expectedFeeMPaid = new BigNumber(0); - const expectedFeeTPaid = new BigNumber(0); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(taker).to.be.equal(logArgs.taker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedFilledMakerTokenAmount).to.be.bignumber.equal(logArgs.filledMakerTokenAmount); - expect(expectedFilledTakerTokenAmount).to.be.bignumber.equal(logArgs.filledTakerTokenAmount); - expect(expectedFeeMPaid).to.be.bignumber.equal(logArgs.paidMakerFee); - expect(expectedFeeTPaid).to.be.bignumber.equal(logArgs.paidTakerFee); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); - }); - - it('should throw when taker is specified and order is claimed by other', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - taker: feeRecipient, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if signature is invalid', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), - }); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeCancelEventArgs>; + const logArgs = log.args; - signedOrder.ecSignature.r = ethUtil.bufferToHex(ethUtil.sha3('invalidR')); - signedOrder.ecSignature.s = ethUtil.bufferToHex(ethUtil.sha3('invalidS')); - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + expect(signedOrder.makerAddress).to.be.equal(logArgs.makerAddress); + expect(signedOrder.makerAddress).to.be.equal(logArgs.senderAddress); + expect(signedOrder.feeRecipientAddress).to.be.equal(logArgs.feeRecipientAddress); + expect(signedOrder.makerAssetData).to.be.equal(logArgs.makerAssetData); + expect(signedOrder.takerAssetData).to.be.equal(logArgs.takerAssetData); + expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); }); - it('should throw if makerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + it('should throw if already cancelled', async () => { + await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, + ); }); - it('should throw if takerTokenAmount is 0', async () => { + it('should throw if order is expired', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: new BigNumber(0), + expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10), }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if fillTakerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: new BigNumber(0), - }), - ).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, + ); }); - it('should not change balances if maker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { + it('should throw if rounding error is greater than 0.1%', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), + makerAssetAmount: new BigNumber(1001), + takerAssetAmount: new BigNumber(3), }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if maker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), + const fillTakerAssetAmount1 = new BigNumber(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: fillTakerAssetAmount1, }); - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + const fillTakerAssetAmount2 = new BigNumber(1); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: fillTakerAssetAmount2, }), - ).to.be.rejectedWith(constants.REVERT); + RevertReason.RoundingError, + ); }); + }); - it('should not change balances if taker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), - }); - - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + describe('cancelOrdersUpTo', () => { + it('should fail to set orderEpoch less than current orderEpoch', async () => { + const orderEpoch = new BigNumber(1); + await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress); + const lesserOrderEpoch = new BigNumber(0); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress), + RevertReason.InvalidNewOrderEpoch, + ); }); - it('should throw if taker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), - }); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, - }), - ).to.be.rejectedWith(constants.REVERT); + it('should fail to set orderEpoch equal to existing orderEpoch', async () => { + const orderEpoch = new BigNumber(1); + await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress), + RevertReason.InvalidNewOrderEpoch, + ); }); - it('should not change balances if maker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: maker }); - await exWrapper.fillOrderAsync(signedOrder, taker); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); + it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => { + // Cancel all transactions with a orderEpoch less than 1 + const orderEpoch = new BigNumber(1); + await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress); - it('should throw if maker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: maker }); - expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + // Create 3 orders with orderEpoch values: 0,1,2,3 + // Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed + erc20Balances = await erc20Wrapper.getBalancesAsync(); + const signedOrders = [ + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18), + salt: new BigNumber(0), }), - ).to.be.rejectedWith(constants.REVERT); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }); - }); - - it('should not change balances if taker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: taker }); - await exWrapper.fillOrderAsync(signedOrder, taker); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if taker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: taker }); - expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18), + salt: new BigNumber(1), }), - ).to.be.rejectedWith(constants.REVERT); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - }); - - it('should not change balances if makerTokenAddress is ZRX, makerTokenAmount + makerFee > maker balance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const makerZRXBalance = new BigNumber(balances[maker][zrx.address]); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAddress: zrx.address, - makerTokenAmount: makerZRXBalance, - makerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if makerTokenAddress is ZRX, makerTokenAmount + makerFee > maker allowance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const makerZRXAllowance = await zrx.allowance.callAsync(maker, tokenTransferProxy.address); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAddress: zrx.address, - makerTokenAmount: new BigNumber(makerZRXAllowance), - makerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if takerTokenAddress is ZRX, takerTokenAmount + takerFee > taker balance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const takerZRXBalance = new BigNumber(balances[taker][zrx.address]); - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: zrx.address, - takerTokenAmount: takerZRXBalance, - takerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if takerTokenAddress is ZRX, takerTokenAmount + takerFee > taker allowance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const takerZRXAllowance = await zrx.allowance.callAsync(taker, tokenTransferProxy.address); - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: zrx.address, - takerTokenAmount: new BigNumber(takerZRXAllowance), - takerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if getBalance or getAllowance attempts to change state and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const maliciousToken = await deployer.deployAsync(ContractName.MaliciousToken); - await maliciousToken.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: maliciousToken.address, - }); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: false, + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18), + salt: new BigNumber(2), }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should not change balances if an order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should log an error event if an order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); - }); - - it('should log an error event if no value is filled', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({}); - await exWrapper.fillOrderAsync(signedOrder, taker); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED_OR_CANCELLED); - }); - }); - - describe('cancelOrder', () => { - beforeEach(async () => { - balances = await dmyBalances.getAsync(); - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - it('should throw if not sent by maker', async () => { - return expect(exWrapper.cancelOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if makerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.cancelOrderAsync(signedOrder, maker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if takerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.cancelOrderAsync(signedOrder, maker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if cancelTakerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - - return expect( - exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount: new BigNumber(0), + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18), + salt: new BigNumber(3), }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should be able to cancel a full order', async () => { - await exWrapper.cancelOrderAsync(signedOrder, maker); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(2), - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should be able to cancel part of an order', async () => { - const cancelTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount, - }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount, + ]; + await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, { + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 490000, }); - const log = res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>; - expect(log.args.filledTakerTokenAmount).to.be.bignumber.equal( - signedOrder.takerTokenAmount.minus(cancelTakerTokenAmount), - ); - const newBalances = await dmyBalances.getAsync(); - const cancelMakerTokenAmount = cancelTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(cancelMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(cancelMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(cancelMakerTokenAmount), + const newBalances = await erc20Wrapper.getBalancesAsync(); + const fillMakerAssetAmount = signedOrders[2].makerAssetAmount.add(signedOrders[3].makerAssetAmount); + const fillTakerAssetAmount = signedOrders[2].takerAssetAmount.add(signedOrders[3].takerAssetAmount); + const makerFee = signedOrders[2].makerFee.add(signedOrders[3].makerFee); + const takerFee = signedOrders[2].takerFee.add(signedOrders[3].takerFee); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(fillMakerAssetAmount), ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(cancelTakerTokenAmount), + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(fillTakerAssetAmount), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(cancelTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(fillTakerAssetAmount), ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(cancelMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(fillMakerAssetAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), ); }); + }); - it('should log 1 event with correct arguments', async () => { - const divisor = 2; - const res = await exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), + describe('Testing Exchange of ERC721 Tokens', () => { + it('should throw when maker does not own the token with id makerAssetId', async () => { + // Construct Exchange parameters + const makerAssetId = erc721TakerAssetIds[0]; + const takerAssetId = erc721TakerAssetIds[1]; + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + }); + // Verify pre-conditions + const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(initialOwnerMakerAsset).to.be.bignumber.not.equal(makerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount; + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.TransferFailed, + ); + }); + + it('should throw when taker does not own the token with id takerAssetId', async () => { + // Construct Exchange parameters + const makerAssetId = erc721MakerAssetIds[0]; + const takerAssetId = erc721MakerAssetIds[1]; + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + }); + // Verify pre-conditions + const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.not.equal(takerAddress); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount; + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.TransferFailed, + ); + }); + + it('should throw when makerAssetAmount is greater than 1', async () => { + // Construct Exchange parameters + const makerAssetId = erc721MakerAssetIds[0]; + const takerAssetId = erc721TakerAssetIds[0]; + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(2), + takerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + }); + // Verify pre-conditions + const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount; + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw when takerAssetAmount is greater than 1', async () => { + // Construct Exchange parameters + const makerAssetId = erc721MakerAssetIds[0]; + const takerAssetId = erc721TakerAssetIds[0]; + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(500), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + }); + // Verify pre-conditions + const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount; + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw on partial fill', async () => { + // Construct Exchange parameters + const makerAssetId = erc721MakerAssetIds[0]; + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), }); - expect(res.logs).to.have.length(1); - - const log = res.logs[0] as LogWithDecodedArgs<LogCancelContractEventArgs>; - const logArgs = log.args; - const expectedCancelledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedCancelledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedCancelledMakerTokenAmount).to.be.bignumber.equal(logArgs.cancelledMakerTokenAmount); - expect(expectedCancelledTakerTokenAmount).to.be.bignumber.equal(logArgs.cancelledTakerTokenAmount); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.RoundingError, + ); }); + }); - it('should not log events if no value is cancelled', async () => { - await exWrapper.cancelOrderAsync(signedOrder, maker); - - const res = await exWrapper.cancelOrderAsync(signedOrder, maker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED_OR_CANCELLED); + describe('getOrderInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); }); - - it('should not log events if order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - - const res = await exWrapper.cancelOrderAsync(signedOrder, maker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + it('should return the correct orderInfo for an unfilled valid order', async () => { + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.FILLABLE; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for a fully filled order', async () => { + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount; + const expectedOrderStatus = OrderStatus.FULLY_FILLED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for a partially filled order', async () => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = takerAssetFillAmount; + const expectedOrderStatus = OrderStatus.FILLABLE; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for a cancelled and unfilled order', async () => { + await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.CANCELLED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for a cancelled and partially filled order', async () => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }); + await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = takerAssetFillAmount; + const expectedOrderStatus = OrderStatus.CANCELLED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for an expired and unfilled order', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); + const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber(); + await increaseTimeAndMineBlockAsync(timeUntilExpiration); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.EXPIRED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for an expired and partially filled order', async () => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }); + const currentTimestamp = await getLatestBlockTimestampAsync(); + const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber(); + await increaseTimeAndMineBlockAsync(timeUntilExpiration); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = takerAssetFillAmount; + const expectedOrderStatus = OrderStatus.EXPIRED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for an expired and fully filled order', async () => { + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + const currentTimestamp = await getLatestBlockTimestampAsync(); + const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber(); + await increaseTimeAndMineBlockAsync(timeUntilExpiration); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount; + // FULLY_FILLED takes precedence over EXPIRED + const expectedOrderStatus = OrderStatus.FULLY_FILLED; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for an order with a makerAssetAmount of 0', async () => { + signedOrder = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(0) }); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.INVALID_MAKER_ASSET_AMOUNT; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + it('should return the correct orderInfo for an order with a takerAssetAmount of 0', async () => { + signedOrder = await orderFactory.newSignedOrderAsync({ takerAssetAmount: new BigNumber(0) }); + const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.INVALID_TAKER_ASSET_AMOUNT; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); }); }); -}); // tslint:disable-line:max-file-line-count +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/dispatcher.ts b/packages/contracts/test/exchange/dispatcher.ts new file mode 100644 index 000000000..81d142ca4 --- /dev/null +++ b/packages/contracts/test/exchange/dispatcher.ts @@ -0,0 +1,226 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { AssetProxyId, RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { + TestAssetProxyDispatcherAssetProxyRegisteredEventArgs, + TestAssetProxyDispatcherContract, +} from '../../generated_contract_wrappers/test_asset_proxy_dispatcher'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('AssetProxyDispatcher', () => { + let owner: string; + let notOwner: string; + let makerAddress: string; + let takerAddress: string; + + let zrxToken: DummyERC20TokenContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + let assetProxyDispatcher: TestAssetProxyDispatcherContract; + + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + // Setup accounts & addresses + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, notOwner, makerAddress, takerAddress] = _.slice(accounts, 0, 4)); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 1; + [zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + + assetProxyDispatcher = await TestAssetProxyDispatcherContract.deployFrom0xArtifactAsync( + artifacts.TestAssetProxyDispatcher, + provider, + txDefaults, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('registerAssetProxy', () => { + it('should record proxy upon registration', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20Proxy.address); + }); + + it('should be able to record multiple proxies', async () => { + // Record first proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + let proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20Proxy.address); + // Record another proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC721); + expect(proxyAddress).to.be.equal(erc721Proxy.address); + }); + + it('should throw if a proxy with the same id is already registered', async () => { + // Initial registration + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20Proxy.address); + // Deploy a new version of the ERC20 Transfer Proxy contract + const newErc20TransferProxy = await ERC20ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC20Proxy, + provider, + txDefaults, + ); + // Register new ERC20 Transfer Proxy contract + return expectTransactionFailedAsync( + assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(newErc20TransferProxy.address, { + from: owner, + }), + RevertReason.AssetProxyAlreadyExists, + ); + }); + + it('should throw if requesting address is not owner', async () => { + return expectTransactionFailedAsync( + assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: notOwner }), + RevertReason.OnlyContractOwner, + ); + }); + + it('should log an event with correct arguments when an asset proxy is registered', async () => { + const logDecoder = new LogDecoder(web3Wrapper, assetProxyDispatcher.address); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + ); + const logs = txReceipt.logs; + const log = logs[0] as LogWithDecodedArgs<TestAssetProxyDispatcherAssetProxyRegisteredEventArgs>; + expect(log.args.id).to.equal(AssetProxyId.ERC20); + expect(log.args.assetProxy).to.equal(erc20Proxy.address); + }); + }); + + describe('getAssetProxy', () => { + it('should return correct address of registered proxy', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20Proxy.address); + }); + + it('should return NULL address if requesting non-existent proxy', async () => { + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(constants.NULL_ADDRESS); + }); + }); + + describe('dispatchTransferFrom', () => { + it('should dispatch transfer to registered proxy', async () => { + // Register ERC20 proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Construct metadata for ERC20 proxy + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync( + encodedAssetData, + makerAddress, + takerAddress, + amount, + { from: owner }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(amount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].add(amount), + ); + }); + + it('should throw if dispatching to unregistered proxy', async () => { + // Construct metadata for ERC20 proxy + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(10); + return expectTransactionFailedAsync( + assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync( + encodedAssetData, + makerAddress, + takerAddress, + amount, + { from: owner }, + ), + RevertReason.AssetProxyDoesNotExist, + ); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts new file mode 100644 index 000000000..1494fe093 --- /dev/null +++ b/packages/contracts/test/exchange/fill_order.ts @@ -0,0 +1,301 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import * as _ from 'lodash'; + +import { chaiSetup } from '../utils/chai_setup'; +import { + FillOrderCombinatorialUtils, + fillOrderCombinatorialUtilsFactoryAsync, +} from '../utils/fill_order_combinatorial_utils'; +import { + AllowanceAmountScenario, + AssetDataScenario, + BalanceAmountScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + FillScenario, + OrderAssetAmountScenario, + TakerAssetFillAmountScenario, + TakerScenario, +} from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const defaultFillScenario = { + orderScenario: { + takerScenario: TakerScenario.Unspecified, + feeRecipientScenario: FeeRecipientAddressScenario.EthUserAddress, + makerAssetAmountScenario: OrderAssetAmountScenario.Large, + takerAssetAmountScenario: OrderAssetAmountScenario.Large, + makerFeeScenario: OrderAssetAmountScenario.Large, + takerFeeScenario: OrderAssetAmountScenario.Large, + expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InFuture, + makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals, + takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount, + makerStateScenario: { + traderAssetBalance: BalanceAmountScenario.Higher, + traderAssetAllowance: AllowanceAmountScenario.Higher, + zrxFeeBalance: BalanceAmountScenario.Higher, + zrxFeeAllowance: AllowanceAmountScenario.Higher, + }, + takerStateScenario: { + traderAssetBalance: BalanceAmountScenario.Higher, + traderAssetAllowance: AllowanceAmountScenario.Higher, + zrxFeeBalance: BalanceAmountScenario.Higher, + zrxFeeAllowance: AllowanceAmountScenario.Higher, + }, +}; + +describe('FillOrder Tests', () => { + let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils; + + before(async () => { + await blockchainLifecycle.startAsync(); + fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('fillOrder', () => { + const test = (fillScenarios: FillScenario[]) => { + _.forEach(fillScenarios, fillScenario => { + const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`; + it(description, async () => { + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + }); + }; + + const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations(); + describe('Combinatorially generated fills orders', () => test(allFillScenarios)); + + it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => { + const fillScenario = { + ...defaultFillScenario, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + takerAssetAmountScenario: OrderAssetAmountScenario.Small, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetAmountScenario: OrderAssetAmountScenario.Small, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + takerScenario: TakerScenario.CorrectlySpecified, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => { + const fillScenario = { + ...defaultFillScenario, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + it('should throw when taker is specified and order is claimed by other', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + takerScenario: TakerScenario.IncorrectlySpecified, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if makerAssetAmount is 0', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetAmountScenario: OrderAssetAmountScenario.Zero, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if takerAssetAmount is 0', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + takerAssetAmountScenario: OrderAssetAmountScenario.Zero, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if takerAssetFillAmount is 0', async () => { + const fillScenario = { + ...defaultFillScenario, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if an order is expired', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if maker erc20Balances are too low to fill order', async () => { + const fillScenario = { + ...defaultFillScenario, + makerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetBalance: BalanceAmountScenario.TooLow, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if taker erc20Balances are too low to fill order', async () => { + const fillScenario = { + ...defaultFillScenario, + takerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetBalance: BalanceAmountScenario.TooLow, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if maker allowances are too low to fill order', async () => { + const fillScenario = { + ...defaultFillScenario, + makerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetAllowance: AllowanceAmountScenario.TooLow, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should throw if taker allowances are too low to fill order', async () => { + const fillScenario = { + ...defaultFillScenario, + takerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetAllowance: AllowanceAmountScenario.TooLow, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + }); + + describe('Testing Exchange of ERC721 Tokens', () => { + it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: AssetDataScenario.ERC721, + takerAssetDataScenario: AssetDataScenario.ERC721, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: AssetDataScenario.ERC721, + takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true); + }); + + it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals, + takerAssetDataScenario: AssetDataScenario.ERC721, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: AssetDataScenario.ERC721, + takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + makerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetAllowance: AllowanceAmountScenario.Unlimited, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + + it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: AssetDataScenario.ERC721, + takerAssetDataScenario: AssetDataScenario.ERC721, + }, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + makerStateScenario: { + ...defaultFillScenario.makerStateScenario, + traderAssetAllowance: AllowanceAmountScenario.Unlimited, + }, + takerStateScenario: { + ...defaultFillScenario.takerStateScenario, + traderAssetAllowance: AllowanceAmountScenario.Unlimited, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); + }); +}); diff --git a/packages/contracts/test/exchange/helpers.ts b/packages/contracts/test/exchange/helpers.ts deleted file mode 100644 index 080cbe5fd..000000000 --- a/packages/contracts/test/exchange/helpers.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import ethUtil = require('ethereumjs-util'); - -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { constants } from '../../util/constants'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; - -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('Exchange', () => { - let maker: string; - let feeRecipient: string; - - let signedOrder: SignedOrder; - let exchangeWrapper: ExchangeWrapper; - let orderFactory: OrderFactory; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - [maker, feeRecipient] = accounts; - const tokenRegistry = await deployer.deployAsync(ContractName.TokenRegistry); - const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const [rep, dgd, zrx] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - const exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress(exchange.address, { from: accounts[0] }); - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('getOrderHash', () => { - it('should output the correct orderHash', async () => { - const orderHashHex = await exchangeWrapper.getOrderHashAsync(signedOrder); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(orderHashHex); - }); - }); - - describe('isValidSignature', () => { - beforeEach(async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - it('should return true with a valid signature', async () => { - const success = await exchangeWrapper.isValidSignatureAsync(signedOrder); - const orderHashHex = ZeroEx.getOrderHashHex(signedOrder); - const isValidSignature = ZeroEx.isValidSignature(orderHashHex, signedOrder.ecSignature, signedOrder.maker); - expect(isValidSignature).to.be.true(); - expect(success).to.be.true(); - }); - - it('should return false with an invalid signature', async () => { - signedOrder.ecSignature.r = ethUtil.bufferToHex(ethUtil.sha3('invalidR')); - signedOrder.ecSignature.s = ethUtil.bufferToHex(ethUtil.sha3('invalidS')); - const success = await exchangeWrapper.isValidSignatureAsync(signedOrder); - const orderHashHex = ZeroEx.getOrderHashHex(signedOrder); - const isValidSignature = ZeroEx.isValidSignature(orderHashHex, signedOrder.ecSignature, signedOrder.maker); - expect(isValidSignature).to.be.false(); - expect(success).to.be.false(); - }); - }); - - describe('isRoundingError', () => { - it('should return false if there is a rounding error of 0.1%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(999); - const target = new BigNumber(50); - // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false if there is a rounding of 0.09%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9991); - const target = new BigNumber(500); - // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return true if there is a rounding error of 0.11%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9989); - const target = new BigNumber(500); - // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return true if there is a rounding error > 0.1%', async () => { - const numerator = new BigNumber(3); - const denominator = new BigNumber(7); - const target = new BigNumber(10); - // rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return false when there is no rounding error', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false when there is rounding error <= 0.1%', async () => { - // randomly generated numbers - const numerator = new BigNumber(76564); - const denominator = new BigNumber(676373677); - const target = new BigNumber(105762562); - // rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) / - // (76564*105762562/676373677) = 0.0007% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - }); - - describe('getPartialAmount', () => { - it('should return the numerator/denominator*target', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 5; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round down', async () => { - const numerator = new BigNumber(2); - const denominator = new BigNumber(3); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 6; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round .5 down', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(20); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 0; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - }); -}); diff --git a/packages/contracts/test/exchange/internal.ts b/packages/contracts/test/exchange/internal.ts new file mode 100644 index 000000000..67d1d2d2c --- /dev/null +++ b/packages/contracts/test/exchange/internal.ts @@ -0,0 +1,305 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { Order, RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals'; +import { artifacts } from '../utils/artifacts'; +import { + getInvalidOpcodeErrorMessageForCallAsync, + getRevertReasonOrErrorMessageForSendTransactionAsync, +} from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from '../utils/combinatorial_utils'; +import { constants } from '../utils/constants'; +import { FillResults } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + +const emptyOrder: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerAssetAmount: new BigNumber(0), + takerAssetAmount: new BigNumber(0), + makerAssetData: '0x', + takerAssetData: '0x', + salt: new BigNumber(0), + exchangeAddress: constants.NULL_ADDRESS, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: new BigNumber(0), +}; + +const emptySignedOrder: SignedOrder = { + ...emptyOrder, + signature: '', +}; + +const overflowErrorForCall = new Error(RevertReason.Uint256Overflow); + +async function referenceGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): Promise<BigNumber> { + const invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + const product = numerator.mul(target); + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + return product.dividedToIntegerBy(denominator); +} + +describe('Exchange core internal functions', () => { + let testExchange: TestExchangeInternalsContract; + let invalidOpcodeErrorForCall: Error | undefined; + let overflowErrorForSendTransaction: Error | undefined; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeInternals, + provider, + txDefaults, + ); + overflowErrorForSendTransaction = new Error( + await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow), + ); + invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + }); + // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset + // the blockchain state for any tests which modify it! + + describe('addFillResults', async () => { + function makeFillResults(value: BigNumber): FillResults { + return { + makerAssetFilledAmount: value, + takerAssetFilledAmount: value, + makerFeePaid: value, + takerFeePaid: value, + }; + } + async function referenceAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise<FillResults> { + // Note(albrow): Here, each of totalFillResults and + // singleFillResults will consist of fields with the same values. + // This should be safe because none of the fields in a given + // FillResults are ever used together in a mathemetical operation. + // They are only used with the corresponding field from *the other* + // FillResults, which are different. + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + // HACK(albrow): _.mergeWith mutates the first argument! To + // workaround this we use _.cloneDeep. + return _.mergeWith( + _.cloneDeep(totalFillResults), + singleFillResults, + (totalVal: BigNumber, singleVal: BigNumber) => { + const newTotal = totalVal.add(singleVal); + if (newTotal.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + return newTotal; + }, + ); + } + async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> { + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'addFillResults', + referenceAddFillResultsAsync, + testAddFillResultsAsync, + [uint256Values, uint256Values], + ); + }); + + describe('calculateFillResults', async () => { + function makeOrder( + makerAssetAmount: BigNumber, + takerAssetAmount: BigNumber, + makerFee: BigNumber, + takerFee: BigNumber, + ): Order { + return { + ...emptyOrder, + makerAssetAmount, + takerAssetAmount, + makerFee, + takerFee, + }; + } + async function referenceCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise<FillResults> { + // Note(albrow): Here we are re-using the same value (otherAmount) + // for order.makerAssetAmount, order.makerFee, and order.takerFee. + // This should be safe because they are never used with each other + // in any mathematical operation in either the reference TypeScript + // implementation or the Solidity implementation of + // calculateFillResults. + return { + makerAssetFilledAmount: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerAssetFilledAmount, + makerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + }; + } + async function testCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise<FillResults> { + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'calculateFillResults', + referenceCalculateFillResultsAsync, + testCalculateFillResultsAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('getPartialAmount', async () => { + async function testGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<BigNumber> { + return testExchange.publicGetPartialAmount.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'getPartialAmount', + referenceGetPartialAmountAsync, + testGetPartialAmountAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('isRoundingError', async () => { + async function referenceIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<boolean> { + const product = numerator.mul(target); + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainder = product.mod(denominator); + if (remainder.eq(0)) { + return false; + } + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (product.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainderTimes1000000 = remainder.mul('1000000'); + if (remainderTimes1000000.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + const errPercentageTimes1000000 = remainderTimes1000000.dividedToIntegerBy(product); + return errPercentageTimes1000000.greaterThan('1000'); + } + async function testIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<boolean> { + return testExchange.publicIsRoundingError.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'isRoundingError', + referenceIsRoundingErrorAsync, + testIsRoundingErrorAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('updateFilledState', async () => { + // Note(albrow): Since updateFilledState modifies the state by calling + // sendTransaction, we must reset the state after each test. + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + async function referenceUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + // tslint:disable-next-line:no-unused-variable + orderHash: string, + ): Promise<BigNumber> { + const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); + if (totalFilledAmount.greaterThan(MAX_UINT256)) { + throw overflowErrorForSendTransaction; + } + return totalFilledAmount; + } + async function testUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + orderHash: string, + ): Promise<BigNumber> { + const fillResults = { + makerAssetFilledAmount: new BigNumber(0), + takerAssetFilledAmount, + makerFeePaid: new BigNumber(0), + takerFeePaid: new BigNumber(0), + }; + await web3Wrapper.awaitTransactionSuccessAsync( + await testExchange.publicUpdateFilledState.sendTransactionAsync( + emptySignedOrder, + constants.NULL_ADDRESS, + orderHash, + orderTakerAssetFilledAmount, + fillResults, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return testExchange.filled.callAsync(orderHash); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'updateFilledState', + referenceUpdateFilledStateAsync, + testUpdateFilledStateAsync, + [uint256Values, uint256Values, bytes32Values], + ); + }); +}); diff --git a/packages/contracts/test/exchange/libs.ts b/packages/contracts/test/exchange/libs.ts new file mode 100644 index 000000000..5c9f9aac7 --- /dev/null +++ b/packages/contracts/test/exchange/libs.ts @@ -0,0 +1,125 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, EIP712Utils, orderHashUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { TestConstantsContract } from '../../generated_contract_wrappers/test_constants'; +import { TestLibsContract } from '../../generated_contract_wrappers/test_libs'; +import { addressUtils } from '../utils/address_utils'; +import { artifacts } from '../utils/artifacts'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { OrderFactory } from '../utils/order_factory'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Exchange libs', () => { + let signedOrder: SignedOrder; + let orderFactory: OrderFactory; + let libs: TestLibsContract; + let testConstants: TestConstantsContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const makerAddress = accounts[0]; + libs = await TestLibsContract.deployFrom0xArtifactAsync(artifacts.TestLibs, provider, txDefaults); + testConstants = await TestConstantsContract.deployFrom0xArtifactAsync( + artifacts.TestConstants, + provider, + txDefaults, + ); + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: libs.address, + makerAddress, + feeRecipientAddress: addressUtils.generatePseudoRandomAddress(), + makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + }); + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('LibConstants', () => { + describe('ZRX_ASSET_DATA', () => { + it('should have the correct ZRX_ASSET_DATA', async () => { + const isValid = await testConstants.assertValidZrxAssetData.callAsync(); + expect(isValid).to.be.equal(true); + }); + }); + }); + // Note(albrow): These tests are designed to be supplemental to the + // combinatorial tests in test/exchange/internal. They test specific edge + // cases that are not covered by the combinatorial tests. + describe('LibMath', () => { + it('should return false if there is a rounding error of 0.1%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(999); + const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return false if there is a rounding of 0.09%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9991); + const target = new BigNumber(500); + // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return true if there is a rounding error of 0.11%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9989); + const target = new BigNumber(500); + // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.true(); + }); + }); + + describe('LibOrder', () => { + describe('getOrderSchema', () => { + it('should output the correct order schema hash', async () => { + const orderSchema = await libs.getOrderSchemaHash.callAsync(); + const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer(); + const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`; + expect(schemaHashHex).to.be.equal(orderSchema); + }); + }); + describe('getDomainSeparatorSchema', () => { + it('should output the correct domain separator schema hash', async () => { + const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync(); + const domainSchemaBuffer = EIP712Utils._getDomainSeparatorSchemaBuffer(); + const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`; + expect(schemaHashHex).to.be.equal(domainSeparatorSchema); + }); + }); + describe('getOrderHash', () => { + it('should output the correct orderHash', async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + const orderHashHex = await libs.publicGetOrderHash.callAsync(signedOrder); + expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(orderHashHex); + }); + }); + }); +}); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts new file mode 100644 index 000000000..46b3569bd --- /dev/null +++ b/packages/contracts/test/exchange/match_orders.ts @@ -0,0 +1,796 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { MatchOrderTester } from '../utils/match_order_tester'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, ERC721TokenIdsByOwner, OrderInfo, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('matchOrders', () => { + let makerAddressLeft: string; + let makerAddressRight: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddressLeft: string; + let feeRecipientAddressRight: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let exchange: ExchangeContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + + let erc20BalancesByOwner: ERC20BalancesByOwner; + let erc721TokenIdsByOwner: ERC721TokenIdsByOwner; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let orderFactoryLeft: OrderFactory; + let orderFactoryRight: OrderFactory; + + let erc721LeftMakerAssetIds: BigNumber[]; + let erc721RightMakerAssetIds: BigNumber[]; + + let defaultERC20MakerAssetAddress: string; + let defaultERC20TakerAssetAddress: string; + let defaultERC721AssetAddress: string; + + let matchOrderTester: MatchOrderTester; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + // Create accounts + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + // Hack(albrow): Both Prettier and TSLint insert a trailing comma below + // but that is invalid syntax as of TypeScript version >= 2.8. We don't + // have the right fine-grained configuration options in TSLint, + // Prettier, or TypeScript, to reconcile this, so we will just have to + // wait for them to sort it out. We disable TSLint and Prettier for + // this part of the code for now. This occurs several times in this + // file. See https://github.com/prettier/prettier/issues/4624. + // prettier-ignore + const usedAddresses = ([ + owner, + makerAddressLeft, + makerAddressRight, + takerAddress, + feeRecipientAddressLeft, + // tslint:disable-next-line:trailing-comma + feeRecipientAddressRight + ] = _.slice(accounts, 0, 6)); + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + // Deploy ERC20 token & ERC20 proxy + const numDummyErc20ToDeploy = 3; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy ERC721 token and proxy + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721LeftMakerAssetIds = erc721Balances[makerAddressLeft][erc721Token.address]; + erc721RightMakerAssetIds = erc721Balances[makerAddressRight][erc721Token.address]; + // Depoy exchange + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(zrxToken.address), + ); + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + // Authorize ERC20 and ERC721 trades by exchange + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Set default addresses + defaultERC20MakerAssetAddress = erc20TokenA.address; + defaultERC20TakerAssetAddress = erc20TokenB.address; + defaultERC721AssetAddress = erc721Token.address; + // Create default order parameters + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParams); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParams); + // Set match order tester + matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, zrxToken.address); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('matchOrders', () => { + beforeEach(async () => { + erc20BalancesByOwner = await erc20Wrapper.getBalancesAsync(); + erc721TokenIdsByOwner = await erc721Wrapper.getBalancesAsync(); + }); + + it('should transfer the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Store original taker balance + const takerInitialBalances = _.cloneDeep(erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress]); + // Match signedOrderLeft with signedOrderRight + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify taker did not take a profit + expect(takerInitialBalances).to.be.deep.equal( + newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + }); + + it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was partially filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + + it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Construct second right order + // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order. + // However, we use 100/50 to ensure a partial fill as we want to go down the "left fill" + // branch in the contract twice for this test. + const signedOrderRight2 = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight2 + const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount; + const rightTakerAssetFilledAmount = new BigNumber(0); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight2, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify left order was fully filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify second right order was partially filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight2); + expect(rightOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Create second left order + // Note: This order needs makerAssetAmount=96/takerAssetAmount=48 to fully fill the right order. + // However, we use 100/50 to ensure a partial fill as we want to go down the "right fill" + // branch in the contract twice for this test. + const signedOrderLeft2 = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + // Match signedOrderLeft2 with signedOrderRight + const leftTakerAssetFilledAmount = new BigNumber(0); + const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus( + erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft2, + signedOrderRight, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify second left order was partially filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft2); + expect(leftOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = feeRecipientAddressLeft; + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left order maker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderLeft.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right order maker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderRight.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left fee recipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressLeft; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressRight; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if left maker is the left fee recipient and right maker is the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: makerAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('Should throw if left order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel left order + await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress); + // Match orders + return expectTransactionFailedAsync( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + RevertReason.OrderUnfillable, + ); + }); + + it('Should throw if right order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel right order + await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress); + // Match orders + return expectTransactionFailedAsync( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + RevertReason.OrderUnfillable, + ); + }); + + it('should throw if there is not a positive spread', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expectTransactionFailedAsync( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + RevertReason.NegativeSpreadRequired, + ); + }); + + it('should throw if the left maker asset is not equal to the right taker asset ', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expectTransactionFailedAsync( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + // We are assuming assetData fields of the right order are the + // reverse of the left order, rather than checking equality. This + // saves a bunch of gas, but as a result if the assetData fields are + // off then the failure ends up happening at signature validation + RevertReason.InvalidOrderSignature, + ); + }); + + it('should throw if the right maker asset is not equal to the left taker asset', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expectTransactionFailedAsync( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + RevertReason.InvalidOrderSignature, + ); + }); + + it('should transfer correct amounts when left order maker asset is an ERC721 token', async () => { + // Create orders to match + const erc721TokenToTransfer = erc721LeftMakerAssetIds[0]; + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + it('should transfer correct amounts when right order maker asset is an ERC721 token', async () => { + // Create orders to match + const erc721TokenToTransfer = erc721RightMakerAssetIds[0]; + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + }); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts new file mode 100644 index 000000000..56a06c247 --- /dev/null +++ b/packages/contracts/test/exchange/signature_validator.ts @@ -0,0 +1,507 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { addSignedMessagePrefix, assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignatureType, SignedOrder, SignerType } from '@0xproject/types'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import ethUtil = require('ethereumjs-util'); + +import { + TestSignatureValidatorContract, + TestSignatureValidatorSignatureValidatorApprovalEventArgs, +} from '../../generated_contract_wrappers/test_signature_validator'; +import { ValidatorContract } from '../../generated_contract_wrappers/validator'; +import { WalletContract } from '../../generated_contract_wrappers/wallet'; +import { addressUtils } from '../utils/address_utils'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { LogDecoder } from '../utils/log_decoder'; +import { OrderFactory } from '../utils/order_factory'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('MixinSignatureValidator', () => { + let signedOrder: SignedOrder; + let orderFactory: OrderFactory; + let signatureValidator: TestSignatureValidatorContract; + let testWallet: WalletContract; + let testValidator: ValidatorContract; + let signerAddress: string; + let signerPrivateKey: Buffer; + let notSignerAddress: string; + let notSignerPrivateKey: Buffer; + let signatureValidatorLogDecoder: LogDecoder; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const makerAddress = accounts[0]; + signerAddress = makerAddress; + notSignerAddress = accounts[1]; + signatureValidator = await TestSignatureValidatorContract.deployFrom0xArtifactAsync( + artifacts.TestSignatureValidator, + provider, + txDefaults, + ); + testWallet = await WalletContract.deployFrom0xArtifactAsync( + artifacts.Wallet, + provider, + txDefaults, + signerAddress, + ); + testValidator = await ValidatorContract.deployFrom0xArtifactAsync( + artifacts.Validator, + provider, + txDefaults, + signerAddress, + ); + signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, signatureValidator.address); + await web3Wrapper.awaitTransactionSuccessAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(testValidator.address, true, { + from: signerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: signatureValidator.address, + makerAddress, + feeRecipientAddress: addressUtils.generatePseudoRandomAddress(), + makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + }; + signerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + notSignerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(notSignerAddress)]; + orderFactory = new OrderFactory(signerPrivateKey, defaultOrderParams); + }); + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('isValidSignature', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + }); + + it('should revert when signature is empty', async () => { + const emptySignature = '0x'; + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + return expectContractCallFailed( + signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + emptySignature, + ), + RevertReason.LengthGreaterThan0Required, + ); + }); + + it('should revert when signature type is unsupported', async () => { + const unsupportedSignatureType = SignatureType.NSignatureTypes; + const unsupportedSignatureHex = '0x' + Buffer.from([unsupportedSignatureType]).toString('hex'); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + return expectContractCallFailed( + signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + unsupportedSignatureHex, + ), + RevertReason.SignatureUnsupported, + ); + }); + + it('should revert when SignatureType=Illegal', async () => { + const unsupportedSignatureHex = '0x' + Buffer.from([SignatureType.Illegal]).toString('hex'); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + return expectContractCallFailed( + signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + unsupportedSignatureHex, + ), + RevertReason.SignatureIllegal, + ); + }); + + it('should return false when SignatureType=Invalid and signature has a length of zero', async () => { + const signatureHex = '0x' + Buffer.from([SignatureType.Invalid]).toString('hex'); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should revert when SignatureType=Invalid and signature length is non-zero', async () => { + const fillerData = ethUtil.toBuffer('0xdeadbeef'); + const signatureType = ethUtil.toBuffer(`0x${SignatureType.Invalid}`); + const signatureBuffer = Buffer.concat([fillerData, signatureType]); + const signatureHex = ethUtil.bufferToHex(signatureBuffer); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + return expectContractCallFailed( + signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + signatureHex, + ), + RevertReason.Length0Required, + ); + }); + + it('should return true when SignatureType=EIP712 and signature is valid', async () => { + // Create EIP712 signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashBuffer = ethUtil.toBuffer(orderHashHex); + const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey); + // Create 0x signature from EIP712 signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.EIP712}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=EIP712 and signature is invalid', async () => { + // Create EIP712 signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashBuffer = ethUtil.toBuffer(orderHashHex); + const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey); + // Create 0x signature from EIP712 signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.EIP712}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature. + // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress` + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + notSignerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=EthSign and signature is valid', async () => { + // Create EthSign signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithEthSignPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Default); + const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey); + // Create 0x signature from EthSign signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.EthSign}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=EthSign and signature is invalid', async () => { + // Create EthSign signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithEthSignPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Default); + const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey); + // Create 0x signature from EthSign signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.EthSign}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature. + // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress` + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + notSignerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=Caller and signer is caller', async () => { + const signature = ethUtil.toBuffer(`0x${SignatureType.Caller}`); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + { from: signerAddress }, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Caller and signer is not caller', async () => { + const signature = ethUtil.toBuffer(`0x${SignatureType.Caller}`); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + { from: notSignerAddress }, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=Wallet and signature is valid', async () => { + // Create EIP712 signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashBuffer = ethUtil.toBuffer(orderHashHex); + const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey); + // Create 0x signature from EIP712 signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Wallet}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + testWallet.address, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Wallet and signature is invalid', async () => { + // Create EIP712 signature using a private key that does not belong to the wallet owner. + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashBuffer = ethUtil.toBuffer(orderHashHex); + const notWalletOwnerPrivateKey = notSignerPrivateKey; + const ecSignature = ethUtil.ecsign(orderHashBuffer, notWalletOwnerPrivateKey); + // Create 0x signature from EIP712 signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Wallet}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + testWallet.address, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=Validator, signature is valid and validator is approved', async () => { + const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`); + const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`); + const signature = Buffer.concat([validatorAddress, signatureType]); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Validator, signature is invalid and validator is approved', async () => { + const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`); + const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`); + const signature = Buffer.concat([validatorAddress, signatureType]); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + // This will return false because we signed the message with `signerAddress`, but + // are validating against `notSignerAddress` + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + notSignerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return false when SignatureType=Validator, signature is valid and validator is not approved', async () => { + // Set approval of signature validator to false + await web3Wrapper.awaitTransactionSuccessAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync( + testValidator.address, + false, + { from: signerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Validate signature + const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`); + const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`); + const signature = Buffer.concat([validatorAddress, signatureType]); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=Trezor and signature is valid', async () => { + // Create Trezor signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithTrezorPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Trezor); + const orderHashWithTrezorPrefixBuffer = ethUtil.toBuffer(orderHashWithTrezorPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithTrezorPrefixBuffer, signerPrivateKey); + // Create 0x signature from Trezor signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Trezor}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Trezor and signature is invalid', async () => { + // Create Trezor signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithTrezorPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Trezor); + const orderHashWithTrezorPrefixBuffer = ethUtil.toBuffer(orderHashWithTrezorPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithTrezorPrefixBuffer, signerPrivateKey); + // Create 0x signature from Trezor signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Trezor}`), + ]); + const signatureHex = ethUtil.bufferToHex(signature); + // Validate signature. + // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress` + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + notSignerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + + it('should return true when SignatureType=Presigned and signer has presigned hash', async () => { + // Presign hash + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + await web3Wrapper.awaitTransactionSuccessAsync( + await signatureValidator.preSign.sendTransactionAsync( + orderHashHex, + signedOrder.makerAddress, + signedOrder.signature, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Validate presigned signature + const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`); + const signatureHex = ethUtil.bufferToHex(signature); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Presigned and signer has not presigned hash', async () => { + const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signedOrder.makerAddress, + signatureHex, + ); + expect(isValidSignature).to.be.false(); + }); + }); + + describe('setSignatureValidatorApproval', () => { + it('should emit a SignatureValidatorApprovalSet with correct args when a validator is approved', async () => { + const approval = true; + const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync( + testValidator.address, + approval, + { + from: signerAddress, + }, + ), + ); + expect(res.logs.length).to.equal(1); + const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>; + const logArgs = log.args; + expect(logArgs.signerAddress).to.equal(signerAddress); + expect(logArgs.validatorAddress).to.equal(testValidator.address); + expect(logArgs.approved).to.equal(approval); + }); + it('should emit a SignatureValidatorApprovalSet with correct args when a validator is disapproved', async () => { + const approval = false; + const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync( + testValidator.address, + approval, + { + from: signerAddress, + }, + ), + ); + expect(res.logs.length).to.equal(1); + const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>; + const logArgs = log.args; + expect(logArgs.signerAddress).to.equal(signerAddress); + expect(logArgs.validatorAddress).to.equal(testValidator.address); + expect(logArgs.approved).to.equal(approval); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts new file mode 100644 index 000000000..2bdd96b16 --- /dev/null +++ b/packages/contracts/test/exchange/transactions.ts @@ -0,0 +1,462 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { OrderWithoutExchangeAddress, RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { ExchangeWrapperContract } from '../../generated_contract_wrappers/exchange_wrapper'; +import { WhitelistContract } from '../../generated_contract_wrappers/whitelist'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { orderUtils } from '../utils/order_utils'; +import { TransactionFactory } from '../utils/transaction_factory'; +import { ERC20BalancesByOwner, SignedTransaction } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Exchange transactions', () => { + let senderAddress: string; + let owner: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipientAddress: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let exchange: ExchangeContract; + let erc20Proxy: ERC20ProxyContract; + + let erc20Balances: ERC20BalancesByOwner; + let signedOrder: SignedOrder; + let signedTx: SignedTransaction; + let orderWithoutExchangeAddress: OrderWithoutExchangeAddress; + let orderFactory: OrderFactory; + let makerTransactionFactory: TransactionFactory; + let takerTransactionFactory: TransactionFactory; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + + let defaultMakerTokenAddress: string; + let defaultTakerTokenAddress: string; + let makerPrivateKey: Buffer; + let takerPrivateKey: Buffer; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = _.slice( + accounts, + 0, + 5, + )); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(zrxToken.address), + ); + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + defaultMakerTokenAddress = erc20TokenA.address; + defaultTakerTokenAddress = erc20TokenB.address; + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + senderAddress, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress), + }; + makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; + orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams); + makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address); + takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address); + }); + describe('executeTransaction', () => { + describe('fillOrder', () => { + let takerAssetFillAmount: BigNumber; + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + signedOrder = await orderFactory.newSignedOrderAsync(); + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + + takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const data = exchange.fillOrder.getABIEncodedTransactionData( + orderWithoutExchangeAddress, + takerAssetFillAmount, + signedOrder.signature, + ); + signedTx = takerTransactionFactory.newSignedTransaction(data); + }); + + it('should throw if not called by specified sender', async () => { + return expectTransactionFailedAsync( + exchangeWrapper.executeTransactionAsync(signedTx, takerAddress), + RevertReason.FailedExecution, + ); + }); + + it('should transfer the correct amounts when signed by taker and called by sender', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFeePaid = signedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFeePaid = signedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + + it('should throw if the a 0x transaction with the same transactionHash has already been executed', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + return expectTransactionFailedAsync( + exchangeWrapper.executeTransactionAsync(signedTx, senderAddress), + RevertReason.InvalidTxHash, + ); + }); + + it('should reset the currentContextAddress', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + const currentContextAddress = await exchange.currentContextAddress.callAsync(); + expect(currentContextAddress).to.equal(constants.NULL_ADDRESS); + }); + }); + + describe('cancelOrder', () => { + beforeEach(async () => { + const data = exchange.cancelOrder.getABIEncodedTransactionData(orderWithoutExchangeAddress); + signedTx = makerTransactionFactory.newSignedTransaction(data); + }); + + it('should throw if not called by specified sender', async () => { + return expectTransactionFailedAsync( + exchangeWrapper.executeTransactionAsync(signedTx, makerAddress), + RevertReason.FailedExecution, + ); + }); + + it('should cancel the order when signed by maker and called by sender', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, senderAddress), + RevertReason.OrderUnfillable, + ); + }); + }); + + describe('cancelOrdersUpTo', () => { + let exchangeWrapperContract: ExchangeWrapperContract; + + before(async () => { + exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync( + artifacts.ExchangeWrapper, + provider, + txDefaults, + exchange.address, + ); + }); + + it("should cancel an order if called from the order's sender", async () => { + const orderSalt = new BigNumber(0); + signedOrder = await orderFactory.newSignedOrderAsync({ + senderAddress: exchangeWrapperContract.address, + salt: orderSalt, + }); + const targetOrderEpoch = orderSalt.add(1); + const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch); + const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData); + await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync( + targetOrderEpoch, + signedCancelTx.salt, + signedCancelTx.signature, + { + from: makerAddress, + }, + ); + + const takerAssetFillAmount = signedOrder.takerAssetAmount; + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + const fillData = exchange.fillOrder.getABIEncodedTransactionData( + orderWithoutExchangeAddress, + takerAssetFillAmount, + signedOrder.signature, + ); + const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData); + return expectTransactionFailedAsync( + exchangeWrapperContract.fillOrder.sendTransactionAsync( + orderWithoutExchangeAddress, + takerAssetFillAmount, + signedFillTx.salt, + signedOrder.signature, + signedFillTx.signature, + { from: takerAddress }, + ), + RevertReason.FailedExecution, + ); + }); + + it("should not cancel an order if not called from the order's sender", async () => { + const orderSalt = new BigNumber(0); + signedOrder = await orderFactory.newSignedOrderAsync({ + senderAddress: exchangeWrapperContract.address, + salt: orderSalt, + }); + const targetOrderEpoch = orderSalt.add(1); + await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress); + + erc20Balances = await erc20Wrapper.getBalancesAsync(); + const takerAssetFillAmount = signedOrder.takerAssetAmount; + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + const data = exchange.fillOrder.getABIEncodedTransactionData( + orderWithoutExchangeAddress, + takerAssetFillAmount, + signedOrder.signature, + ); + signedTx = takerTransactionFactory.newSignedTransaction(data); + await exchangeWrapperContract.fillOrder.sendTransactionAsync( + orderWithoutExchangeAddress, + takerAssetFillAmount, + signedTx.salt, + signedOrder.signature, + signedTx.signature, + { from: takerAddress }, + ); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFeePaid = signedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFeePaid = signedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + }); + }); + + describe('Whitelist', () => { + let whitelist: WhitelistContract; + let whitelistOrderFactory: OrderFactory; + + before(async () => { + whitelist = await WhitelistContract.deployFrom0xArtifactAsync( + artifacts.Whitelist, + provider, + txDefaults, + exchange.address, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await exchange.setSignatureValidatorApproval.sendTransactionAsync(whitelist.address, isApproved, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + senderAddress: whitelist.address, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress), + }; + whitelistOrderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams); + }); + + beforeEach(async () => { + signedOrder = await whitelistOrderFactory.newSignedOrderAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + + it('should revert if maker has not been whitelisted', async () => { + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + const takerAssetFillAmount = signedOrder.takerAssetAmount; + const salt = generatePseudoRandomSalt(); + return expectTransactionFailedAsync( + whitelist.fillOrderIfWhitelisted.sendTransactionAsync( + orderWithoutExchangeAddress, + takerAssetFillAmount, + salt, + signedOrder.signature, + { from: takerAddress }, + ), + RevertReason.MakerNotWhitelisted, + ); + }); + + it('should revert if taker has not been whitelisted', async () => { + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + const takerAssetFillAmount = signedOrder.takerAssetAmount; + const salt = generatePseudoRandomSalt(); + return expectTransactionFailedAsync( + whitelist.fillOrderIfWhitelisted.sendTransactionAsync( + orderWithoutExchangeAddress, + takerAssetFillAmount, + salt, + signedOrder.signature, + { from: takerAddress }, + ), + RevertReason.TakerNotWhitelisted, + ); + }); + + it('should fill the order if maker and taker have been whitelisted', async () => { + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + await web3Wrapper.awaitTransactionSuccessAsync( + await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + const takerAssetFillAmount = signedOrder.takerAssetAmount; + const salt = generatePseudoRandomSalt(); + await web3Wrapper.awaitTransactionSuccessAsync( + await whitelist.fillOrderIfWhitelisted.sendTransactionAsync( + orderWithoutExchangeAddress, + takerAssetFillAmount, + salt, + signedOrder.signature, + { from: takerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAssetFillAmount = signedOrder.makerAssetAmount; + const makerFeePaid = signedOrder.makerFee; + const takerFeePaid = signedOrder.takerFee; + + expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + }); +}); diff --git a/packages/contracts/test/exchange/wrapper.ts b/packages/contracts/test/exchange/wrapper.ts index 0d3e18711..d48441dca 100644 --- a/packages/contracts/test/exchange/wrapper.ts +++ b/packages/contracts/test/exchange/wrapper.ts @@ -1,385 +1,1259 @@ -import { SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; import * as _ from 'lodash'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { TokenRegistryContract } from '../../src/contract_wrappers/generated/token_registry'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName } from '../../util/types'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -describe('Exchange', () => { - let maker: string; - let tokenOwner: string; - let taker: string; - let feeRecipient: string; - - const INIT_BAL = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INIT_ALLOW = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +describe('Exchange wrappers', () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; - let rep: DummyTokenContract; - let dgd: DummyTokenContract; - let zrx: DummyTokenContract; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; let exchange: ExchangeContract; - let tokenRegistry: TokenRegistryContract; - let tokenTransferProxy: TokenTransferProxyContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; - let balances: BalancesByOwner; - - let exWrapper: ExchangeWrapper; - let dmyBalances: Balances; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let erc20Balances: ERC20BalancesByOwner; let orderFactory: OrderFactory; + let erc721MakerAssetId: BigNumber; + let erc721TakerAssetId: BigNumber; + + let defaultMakerAssetAddress: string; + let defaultTakerAssetAddress: string; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - tokenOwner = accounts[0]; - [maker, taker, feeRecipient] = accounts; - const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - dgd = new DummyTokenContract(web3Wrapper, dgdInstance.abi, dgdInstance.address); - zrx = new DummyTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - const tokenRegistryInstance = await deployer.deployAsync(ContractName.TokenRegistry); - tokenRegistry = new TokenRegistryContract( - web3Wrapper, - tokenRegistryInstance.abi, - tokenRegistryInstance.address, + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4)); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, ); - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetId = erc721Balances[makerAddress][erc721Token.address][0]; + erc721TakerAssetId = erc721Balances[takerAddress][erc721Token.address][0]; + + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(zrxToken.address), ); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - exWrapper = new ExchangeWrapper(exchange, zeroEx); + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), }; - - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - dmyBalances = new Balances([rep, dgd, zrx], [maker, taker, feeRecipient]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - rep.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - rep.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - dgd.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - dgd.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - zrx.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - zrx.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - ]); + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); }); afterEach(async () => { await blockchainLifecycle.revertAsync(); }); describe('fillOrKillOrder', () => { - beforeEach(async () => { - balances = await dmyBalances.getAsync(); + it('should transfer the correct amounts', async () => { + const signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + }); + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + await exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFee = signedOrder.makerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFee = signedOrder.takerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount), + ); + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), + ); + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), + ); }); - it('should transfer the correct amounts', async () => { + it('should throw if a signedOrder is expired', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); const signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10), }); - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrKillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount, + + return expectTransactionFailedAsync( + exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress), + RevertReason.OrderUnfillable, + ); + }); + + it('should throw if entire takerAssetFillAmount not filled', async () => { + const signedOrder = await orderFactory.newSignedOrderAsync(); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: signedOrder.takerAssetAmount.div(2), }); - const newBalances = await dmyBalances.getAsync(); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress), + RevertReason.CompleteFillFailed, + ); + }); + }); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + describe('fillOrderNoThrow', () => { + it('should transfer the correct amounts', async () => { + const signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + }); + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 250000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); const makerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount), + ); + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal(balances[maker][zrx.address].minus(makerFee)); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal(balances[taker][zrx.address].minus(takerFee)); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(makerFee.add(takerFee)), + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), ); }); - it('should throw if an signedOrder is expired', async () => { + it('should not change erc20Balances if maker erc20Balances are too low to fill order', async () => { const signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18), }); - return expect(exWrapper.fillOrKillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); - it('should throw if entire fillTakerTokenAmount not filled', async () => { + it('should not change erc20Balances if taker erc20Balances are too low to fill order', async () => { + const signedOrder = await orderFactory.newSignedOrderAsync({ + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18), + }); + + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not change erc20Balances if maker allowances are too low to fill order', async () => { const signedOrder = await orderFactory.newSignedOrderAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); - const from = taker; - await exWrapper.fillOrderAsync(signedOrder, from, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(2), + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not change erc20Balances if taker allowances are too low to fill order', async () => { + const signedOrder = await orderFactory.newSignedOrderAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker balance', async () => { + const makerZRXBalance = new BigNumber(erc20Balances[makerAddress][zrxToken.address]); + const signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: makerZRXBalance, + makerFee: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), }); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker allowance', async () => { + const makerZRXAllowance = await zrxToken.allowance.callAsync(makerAddress, erc20Proxy.address); + const signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(makerZRXAllowance), + makerFee: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); - return expect(exWrapper.fillOrKillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker balance', async () => { + const takerZRXBalance = new BigNumber(erc20Balances[takerAddress][zrxToken.address]); + const signedOrder = await orderFactory.newSignedOrderAsync({ + takerAssetAmount: takerZRXBalance, + takerFee: new BigNumber(1), + takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker allowance', async () => { + const takerZRXAllowance = await zrxToken.allowance.callAsync(takerAddress, erc20Proxy.address); + const signedOrder = await orderFactory.newSignedOrderAsync({ + takerAssetAmount: new BigNumber(takerZRXAllowance), + takerFee: new BigNumber(1), + takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should successfully exchange ERC721 tokens', async () => { + // Construct Exchange parameters + const makerAssetId = erc721MakerAssetId; + const takerAssetId = erc721TakerAssetId; + const signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + }); + // Verify pre-conditions + const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount; + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 280000, + }); + // Verify post-conditions + const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(newOwnerTakerAsset).to.be.bignumber.equal(makerAddress); }); }); describe('batch functions', () => { let signedOrders: SignedOrder[]; beforeEach(async () => { - signedOrders = await Promise.all([ - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync(), - ]); - balances = await dmyBalances.getAsync(); + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + ]; }); describe('batchFillOrders', () => { it('should transfer the correct amounts', async () => { - const fillTakerTokenAmounts: BigNumber[] = []; - const makerTokenAddress = rep.address; - const takerTokenAddress = dgd.address; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + const takerAssetFillAmounts: BigNumber[] = []; + const makerAssetAddress = erc20TokenA.address; + const takerAssetAddress = erc20TokenB.address; + _.forEach(signedOrders, signedOrder => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); const makerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - fillTakerTokenAmounts.push(fillTakerTokenAmount); - balances[maker][makerTokenAddress] = balances[maker][makerTokenAddress].minus(fillMakerTokenAmount); - balances[maker][takerTokenAddress] = balances[maker][takerTokenAddress].add(fillTakerTokenAmount); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(makerFee); - balances[taker][makerTokenAddress] = balances[taker][makerTokenAddress].add(fillMakerTokenAmount); - balances[taker][takerTokenAddress] = balances[taker][takerTokenAddress].minus(fillTakerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - makerFee.add(takerFee), + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + takerAssetFillAmounts.push(takerAssetFillAmount); + erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][ + makerAssetAddress + ].minus(makerAssetFilledAmount); + erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add( + takerAssetFillAmount, ); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + makerFee, + ); + erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add( + makerAssetFilledAmount, + ); + erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][ + takerAssetAddress + ].minus(takerAssetFillAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(makerFee.add(takerFee)); }); - await exWrapper.batchFillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); }); describe('batchFillOrKillOrders', () => { it('should transfer the correct amounts', async () => { - const fillTakerTokenAmounts: BigNumber[] = []; - const makerTokenAddress = rep.address; - const takerTokenAddress = dgd.address; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + const takerAssetFillAmounts: BigNumber[] = []; + const makerAssetAddress = erc20TokenA.address; + const takerAssetAddress = erc20TokenB.address; + _.forEach(signedOrders, signedOrder => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); const makerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - fillTakerTokenAmounts.push(fillTakerTokenAmount); - balances[maker][makerTokenAddress] = balances[maker][makerTokenAddress].minus(fillMakerTokenAmount); - balances[maker][takerTokenAddress] = balances[maker][takerTokenAddress].add(fillTakerTokenAmount); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(makerFee); - balances[taker][makerTokenAddress] = balances[taker][makerTokenAddress].add(fillMakerTokenAmount); - balances[taker][takerTokenAddress] = balances[taker][takerTokenAddress].minus(fillTakerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - makerFee.add(takerFee), + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + takerAssetFillAmounts.push(takerAssetFillAmount); + erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][ + makerAssetAddress + ].minus(makerAssetFilledAmount); + erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add( + takerAssetFillAmount, + ); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + makerFee, ); + erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add( + makerAssetFilledAmount, + ); + erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][ + takerAssetAddress + ].minus(takerAssetFillAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(makerFee.add(takerFee)); }); - await exWrapper.batchFillOrKillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + await exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); it('should throw if a single signedOrder does not fill the expected amount', async () => { - const fillTakerTokenAmounts: BigNumber[] = []; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - fillTakerTokenAmounts.push(fillTakerTokenAmount); + const takerAssetFillAmounts: BigNumber[] = []; + _.forEach(signedOrders, signedOrder => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + takerAssetFillAmounts.push(takerAssetFillAmount); + }); + + await exchangeWrapper.fillOrKillOrderAsync(signedOrders[0], takerAddress); + + return expectTransactionFailedAsync( + exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, + }), + RevertReason.OrderUnfillable, + ); + }); + }); + + describe('batchFillOrdersNoThrow', async () => { + it('should transfer the correct amounts', async () => { + const takerAssetFillAmounts: BigNumber[] = []; + const makerAssetAddress = erc20TokenA.address; + const takerAssetAddress = erc20TokenB.address; + _.forEach(signedOrders, signedOrder => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFee = signedOrder.makerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFee = signedOrder.takerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + takerAssetFillAmounts.push(takerAssetFillAmount); + erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][ + makerAssetAddress + ].minus(makerAssetFilledAmount); + erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add( + takerAssetFillAmount, + ); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + makerFee, + ); + erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add( + makerAssetFilledAmount, + ); + erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][ + takerAssetAddress + ].minus(takerAssetFillAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(makerFee.add(takerFee)); + }); + + await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not throw if an order is invalid and fill the remaining orders', async () => { + const takerAssetFillAmounts: BigNumber[] = []; + const makerAssetAddress = erc20TokenA.address; + const takerAssetAddress = erc20TokenB.address; + + const invalidOrder = { + ...signedOrders[0], + signature: '0x00', + }; + const validOrders = signedOrders.slice(1); + + takerAssetFillAmounts.push(invalidOrder.takerAssetAmount.div(2)); + _.forEach(validOrders, signedOrder => { + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const makerAssetFilledAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFee = signedOrder.makerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFee = signedOrder.takerFee + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + takerAssetFillAmounts.push(takerAssetFillAmount); + erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][ + makerAssetAddress + ].minus(makerAssetFilledAmount); + erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add( + takerAssetFillAmount, + ); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + makerFee, + ); + erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add( + makerAssetFilledAmount, + ); + erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][ + takerAssetAddress + ].minus(takerAssetFillAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(makerFee.add(takerFee)); + }); + + const newOrders = [invalidOrder, ...validOrders]; + await exchangeWrapper.batchFillOrdersNoThrowAsync(newOrders, takerAddress, { + takerAssetFillAmounts, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 450000, }); - await exWrapper.fillOrKillOrderAsync(signedOrders[0], taker); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + }); + + describe('marketSellOrders', () => { + it('should stop when the entire takerAssetFillAmount is filled', async () => { + const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus( + signedOrders[1].takerAssetAmount.div(2), + ); + await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmount, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); - return expect( - exWrapper.batchFillOrKillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add( + signedOrders[1].makerAssetAmount.dividedToIntegerBy(2), + ); + const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2)); + const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount), + ); + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), + ); + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), + ); + }); + + it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => { + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + _.forEach(signedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, + ); + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); + }); + await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmount, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should throw when a signedOrder does not use the same takerAssetAddress', async () => { + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync({ + takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }), + await orderFactory.newSignedOrderAsync(), + ]; + + return expectTransactionFailedAsync( + exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18), }), - ).to.be.rejectedWith(constants.REVERT); + // We simply use the takerAssetData from the first order for all orders. + // If they are not the same, the contract throws when validating the order signature + RevertReason.InvalidOrderSignature, + ); }); }); - describe('fillOrdersUpTo', () => { - it('should stop when the entire fillTakerTokenAmount is filled', async () => { - const fillTakerTokenAmount = signedOrders[0].takerTokenAmount.plus( - signedOrders[1].takerTokenAmount.div(2), + describe('marketSellOrdersNoThrow', () => { + it('should stop when the entire takerAssetFillAmount is filled', async () => { + const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus( + signedOrders[1].takerAssetAmount.div(2), ); - await exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount, + await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 6000000, }); - const newBalances = await dmyBalances.getAsync(); + const newBalances = await erc20Wrapper.getBalancesAsync(); - const fillMakerTokenAmount = signedOrders[0].makerTokenAmount.add( - signedOrders[1].makerTokenAmount.dividedToIntegerBy(2), + const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add( + signedOrders[1].makerAssetAmount.dividedToIntegerBy(2), ); const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2)); const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2)); - expect(newBalances[maker][signedOrders[0].makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrders[0].makerTokenAddress].minus(fillMakerTokenAmount), + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount), ); - expect(newBalances[maker][signedOrders[0].takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrders[0].takerTokenAddress].add(fillTakerTokenAmount), + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(makerFee), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[taker][signedOrders[0].takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrders[0].takerTokenAddress].minus(fillTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), ); - expect(newBalances[taker][signedOrders[0].makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrders[0].makerTokenAddress].add(fillMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(takerFee), + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(makerFee.add(takerFee)), + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), ); }); - it('should fill all signedOrders if cannot fill entire fillTakerTokenAmount', async () => { - const fillTakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18); - signedOrders.forEach(signedOrder => { - balances[maker][signedOrder.makerTokenAddress] = balances[maker][ - signedOrder.makerTokenAddress - ].minus(signedOrder.makerTokenAmount); - balances[maker][signedOrder.takerTokenAddress] = balances[maker][signedOrder.takerTokenAddress].add( - signedOrder.takerTokenAmount, + it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => { + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + _.forEach(signedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, + ); + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); + }); + await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not fill a signedOrder that does not use the same takerAssetAddress', async () => { + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync({ + takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }), + ]; + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + const filledSignedOrders = signedOrders.slice(0, -1); + _.forEach(filledSignedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, ); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(signedOrder.makerFee); - balances[taker][signedOrder.makerTokenAddress] = balances[taker][signedOrder.makerTokenAddress].add( - signedOrder.makerTokenAmount, + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, ); - balances[taker][signedOrder.takerTokenAddress] = balances[taker][ - signedOrder.takerTokenAddress - ].minus(signedOrder.takerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(signedOrder.takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - signedOrder.makerFee.add(signedOrder.takerFee), + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); + }); + await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + }); + + describe('marketBuyOrders', () => { + it('should stop when the entire makerAssetFillAmount is filled', async () => { + const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus( + signedOrders[1].makerAssetAmount.div(2), + ); + await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, { + makerAssetFillAmount, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAmountBought = signedOrders[0].takerAssetAmount.add( + signedOrders[1].takerAssetAmount.dividedToIntegerBy(2), + ); + const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2)); + const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), + ); + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), + ); + }); + + it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => { + const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + _.forEach(signedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, + ); + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); }); - await exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount, + await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, { + makerAssetFillAmount, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); - it('should throw when an signedOrder does not use the same takerTokenAddress', async () => { - signedOrders = await Promise.all([ - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync({ takerTokenAddress: zrx.address }), - orderFactory.newSignedOrderAsync(), - ]); + it('should throw when a signedOrder does not use the same makerAssetAddress', async () => { + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }), + await orderFactory.newSignedOrderAsync(), + ]; - return expect( - exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1000), 18), + return expectTransactionFailedAsync( + exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, { + makerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18), }), - ).to.be.rejectedWith(constants.REVERT); + RevertReason.InvalidOrderSignature, + ); + }); + }); + + describe('marketBuyOrdersNoThrow', () => { + it('should stop when the entire makerAssetFillAmount is filled', async () => { + const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus( + signedOrders[1].makerAssetAmount.div(2), + ); + await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, { + makerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAmountBought = signedOrders[0].takerAssetAmount.add( + signedOrders[1].takerAssetAmount.dividedToIntegerBy(2), + ); + const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2)); + const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), + ); + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), + ); + }); + + it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => { + const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + _.forEach(signedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, + ); + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); + }); + await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, { + makerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + + it('should not fill a signedOrder that does not use the same makerAssetAddress', async () => { + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + }), + ]; + + const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18); + const filledSignedOrders = signedOrders.slice(0, -1); + _.forEach(filledSignedOrders, signedOrder => { + erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][ + defaultMakerAssetAddress + ].minus(signedOrder.makerAssetAmount); + erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][ + defaultTakerAssetAddress + ].add(signedOrder.takerAssetAmount); + erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus( + signedOrder.makerFee, + ); + erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][ + defaultMakerAssetAddress + ].add(signedOrder.makerAssetAmount); + erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][ + defaultTakerAssetAddress + ].minus(signedOrder.takerAssetAmount); + erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus( + signedOrder.takerFee, + ); + erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][ + zrxToken.address + ].add(signedOrder.makerFee.add(signedOrder.takerFee)); + }); + await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, { + makerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }); + + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); }); describe('batchCancelOrders', () => { it('should be able to cancel multiple signedOrders', async () => { - const cancelTakerTokenAmounts = _.map(signedOrders, signedOrder => signedOrder.takerTokenAmount); - await exWrapper.batchCancelOrdersAsync(signedOrders, maker, { - cancelTakerTokenAmounts, + const takerAssetCancelAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount); + await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress); + + await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, { + takerAssetFillAmounts: takerAssetCancelAmounts, }); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(erc20Balances).to.be.deep.equal(newBalances); + }); + }); - await exWrapper.batchFillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts: cancelTakerTokenAmounts, + describe('getOrdersInfo', () => { + beforeEach(async () => { + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + ]; + }); + it('should get the correct information for multiple unfilled orders', async () => { + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.FILLABLE; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for multiple partially filled orders', async () => { + const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2)); + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts }); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2); + const expectedOrderStatus = OrderStatus.FILLABLE; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for multiple fully filled orders', async () => { + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount; + const expectedOrderStatus = OrderStatus.FULLY_FILLED; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for multiple cancelled and unfilled orders', async () => { + await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.CANCELLED; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for multiple cancelled and partially filled orders', async () => { + const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2)); + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts }); + await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2); + const expectedOrderStatus = OrderStatus.CANCELLED; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for multiple expired and unfilled orders', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); + const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber(); + await increaseTimeAndMineBlockAsync(timeUntilExpiration); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = new BigNumber(0); + const expectedOrderStatus = OrderStatus.EXPIRED; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); }); - const newBalances = await dmyBalances.getAsync(); - expect(balances).to.be.deep.equal(newBalances); + }); + it('should get the correct information for multiple expired and partially filled orders', async () => { + const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2)); + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts }); + const currentTimestamp = await getLatestBlockTimestampAsync(); + const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber(); + await increaseTimeAndMineBlockAsync(timeUntilExpiration); + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(3); + _.forEach(signedOrders, (signedOrder, index) => { + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2); + const expectedOrderStatus = OrderStatus.EXPIRED; + const orderInfo = ordersInfo[index]; + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount); + expect(orderInfo.orderStatus).to.equal(expectedOrderStatus); + }); + }); + it('should get the correct information for a mix of unfilled, partially filled, fully filled, cancelled, and expired orders', async () => { + const unfilledOrder = await orderFactory.newSignedOrderAsync(); + const partiallyFilledOrder = await orderFactory.newSignedOrderAsync(); + await exchangeWrapper.fillOrderAsync(partiallyFilledOrder, takerAddress, { + takerAssetFillAmount: partiallyFilledOrder.takerAssetAmount.div(2), + }); + const fullyFilledOrder = await orderFactory.newSignedOrderAsync(); + await exchangeWrapper.fillOrderAsync(fullyFilledOrder, takerAddress); + const cancelledOrder = await orderFactory.newSignedOrderAsync(); + await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress); + const currentTimestamp = await getLatestBlockTimestampAsync(); + const expiredOrder = await orderFactory.newSignedOrderAsync({ + expirationTimeSeconds: new BigNumber(currentTimestamp), + }); + signedOrders = [unfilledOrder, partiallyFilledOrder, fullyFilledOrder, cancelledOrder, expiredOrder]; + const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders); + expect(ordersInfo.length).to.be.equal(5); + + const expectedUnfilledOrderHash = orderHashUtils.getOrderHashHex(unfilledOrder); + const expectedUnfilledTakerAssetFilledAmount = new BigNumber(0); + const expectedUnfilledOrderStatus = OrderStatus.FILLABLE; + const unfilledOrderInfo = ordersInfo[0]; + expect(unfilledOrderInfo.orderHash).to.be.equal(expectedUnfilledOrderHash); + expect(unfilledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal( + expectedUnfilledTakerAssetFilledAmount, + ); + expect(unfilledOrderInfo.orderStatus).to.be.equal(expectedUnfilledOrderStatus); + + const expectedPartialOrderHash = orderHashUtils.getOrderHashHex(partiallyFilledOrder); + const expectedPartialTakerAssetFilledAmount = partiallyFilledOrder.takerAssetAmount.div(2); + const expectedPartialOrderStatus = OrderStatus.FILLABLE; + const partialOrderInfo = ordersInfo[1]; + expect(partialOrderInfo.orderHash).to.be.equal(expectedPartialOrderHash); + expect(partialOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal( + expectedPartialTakerAssetFilledAmount, + ); + expect(partialOrderInfo.orderStatus).to.be.equal(expectedPartialOrderStatus); + + const expectedFilledOrderHash = orderHashUtils.getOrderHashHex(fullyFilledOrder); + const expectedFilledTakerAssetFilledAmount = fullyFilledOrder.takerAssetAmount; + const expectedFilledOrderStatus = OrderStatus.FULLY_FILLED; + const filledOrderInfo = ordersInfo[2]; + expect(filledOrderInfo.orderHash).to.be.equal(expectedFilledOrderHash); + expect(filledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal( + expectedFilledTakerAssetFilledAmount, + ); + expect(filledOrderInfo.orderStatus).to.be.equal(expectedFilledOrderStatus); + + const expectedCancelledOrderHash = orderHashUtils.getOrderHashHex(cancelledOrder); + const expectedCancelledTakerAssetFilledAmount = new BigNumber(0); + const expectedCancelledOrderStatus = OrderStatus.CANCELLED; + const cancelledOrderInfo = ordersInfo[3]; + expect(cancelledOrderInfo.orderHash).to.be.equal(expectedCancelledOrderHash); + expect(cancelledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal( + expectedCancelledTakerAssetFilledAmount, + ); + expect(cancelledOrderInfo.orderStatus).to.be.equal(expectedCancelledOrderStatus); + + const expectedExpiredOrderHash = orderHashUtils.getOrderHashHex(expiredOrder); + const expectedExpiredTakerAssetFilledAmount = new BigNumber(0); + const expectedExpiredOrderStatus = OrderStatus.EXPIRED; + const expiredOrderInfo = ordersInfo[4]; + expect(expiredOrderInfo.orderHash).to.be.equal(expectedExpiredOrderHash); + expect(expiredOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal( + expectedExpiredTakerAssetFilledAmount, + ); + expect(expiredOrderInfo.orderStatus).to.be.equal(expectedExpiredOrderStatus); }); }); }); -}); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts new file mode 100644 index 000000000..28ffdeabe --- /dev/null +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -0,0 +1,996 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; +import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { ForwarderWrapper } from '../utils/forwarder_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ContractName, ERC20BalancesByOwner } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; +const MAX_WETH_FILL_PERCENTAGE = 95; + +describe(ContractName.Forwarder, () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + let otherAddress: string; + let defaultMakerAssetAddress: string; + let zrxAssetData: string; + + let weth: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let forwarderContract: ForwarderContract; + let wethContract: WETH9Contract; + let forwarderWrapper: ForwarderWrapper; + let exchangeWrapper: ExchangeWrapper; + + let orderWithoutFee: SignedOrder; + let orderWithFee: SignedOrder; + let feeOrder: SignedOrder; + let orderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let tx: TransactionReceiptWithDecodedLogs; + + let erc721MakerAssetIds: BigNumber[]; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + let gasPrice: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + let erc20TokenA; + [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + + wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, txDefaults); + weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); + erc20Wrapper.addDummyTokenContract(weth); + + const wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeContract = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); + exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + + defaultMakerAssetAddress = erc20TokenA.address; + const defaultTakerAssetAddress = wethContract.address; + const defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + + const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + wethContract.address, + zrxToken.address, + zrxAssetData, + wethAssetData, + ); + forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); + forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); + const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), + ); + erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + orderWithoutFee = await orderFactory.newSignedOrderAsync(); + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('marketSellOrdersWithEth without extra fees', () => { + it('should fill a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const firstTakerAssetFillAmount = ordersWithoutFee[0].takerAssetAmount; + const secondTakerAssetFillAmount = primaryTakerAssetFillAmount.minus(firstTakerAssetFillAmount); + + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount + .times(secondTakerAssetFillAmount) + .dividedToIntegerBy(ordersWithoutFee[1].takerAssetAmount), + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order and pay ZRX fees from a single feeOrder', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue( + orderWithFee.takerFee.dividedToIntegerBy(2), + MAX_WETH_FILL_PERCENTAGE, + ); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the orders and pay ZRX from multiple feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const ethValue = orderWithFee.takerAssetAmount; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue(orderWithFee.takerFee, MAX_WETH_FILL_PERCENTAGE); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order when token is ZRX with fees', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); + const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(ethValue), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.times(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + it('should revert if ZRX cannot be fully repurchased', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should not fill orders with different makerAssetData than the first order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const feeOrders: SignedOrder[] = []; + const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + }); + describe('marketSellOrdersWithEth with extra fees', () => { + it('should fill the order and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 5; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('marketBuyOrdersWithEth without extra fees', () => { + it('should buy the exact amount of makerAsset in a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset in multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount.dividedToIntegerBy(2), + ); + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and return excess ETH', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy slightly greater than makerAssetAmount when buying ZRX', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders( + makerAssetFillAmount, + ordersWithFee, + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + const makerAssetFilledAmount = orderWithFee.makerAssetAmount + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const takerFeePaid = orderWithFee.takerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const makerFeePaid = orderWithFee.makerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid); + // Up to 1 wei worth of ZRX will be overbought per order + const maxOverboughtZrx = new BigNumber(1) + .times(orderWithFee.makerAssetAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + + expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount); + expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(4); + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset from a single order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + from: takerAddress, + value: ethValue, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if buying an ERC721 asset when later orders contain different makerAssetData', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const differentMakerAssetDataOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, differentMakerAssetDataOrder]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1).plus(differentMakerAssetDataOrder.makerAssetAmount); + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('marketBuyOrdersWithEth with extra fees', () => { + it('should buy an asset and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('withdrawAsset', () => { + it('should allow owner to withdraw ERC20 tokens', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: owner }); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[owner][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[owner][zrxToken.address].plus(zrxWithdrawAmount), + ); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address].minus(zrxWithdrawAmount), + ); + }); + it('should revert if not called by owner', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await expectTransactionFailedAsync( + forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: makerAddress }), + RevertReason.OnlyContractOwner, + ); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/global_hooks.ts b/packages/contracts/test/global_hooks.ts index 089521d94..cf7c52efd 100644 --- a/packages/contracts/test/global_hooks.ts +++ b/packages/contracts/test/global_hooks.ts @@ -1,8 +1,15 @@ -import { coverage, env, EnvVars } from '@0xproject/dev-utils'; +import { env, EnvVars } from '@0xproject/dev-utils'; + +import { coverage } from './utils/coverage'; +import { profiler } from './utils/profiler'; after('generate coverage report', async () => { if (env.parseBoolean(EnvVars.SolidityCoverage)) { const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); await coverageSubprovider.writeCoverageAsync(); } + if (env.parseBoolean(EnvVars.SolidityProfiler)) { + const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + await profilerSubprovider.writeProfilerOutputAsync(); + } }); diff --git a/packages/contracts/test/libraries/lib_bytes.ts b/packages/contracts/test/libraries/lib_bytes.ts new file mode 100644 index 000000000..1c497a226 --- /dev/null +++ b/packages/contracts/test/libraries/lib_bytes.ts @@ -0,0 +1,834 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { TestLibBytesContract } from '../../generated_contract_wrappers/test_lib_bytes'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { typeEncodingUtils } from '../utils/type_encoding_utils'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +// BUG: Ideally we would use Buffer.from(memory).toString('hex') +// https://github.com/Microsoft/TypeScript/issues/23155 +const toHex = (buf: Uint8Array): string => buf.reduce((a, v) => a + ('00' + v.toString(16)).slice(-2), '0x'); + +const fromHex = (str: string): Uint8Array => Uint8Array.from(Buffer.from(str.slice(2), 'hex')); + +describe('LibBytes', () => { + let libBytes: TestLibBytesContract; + const byteArrayShorterThan32Bytes = '0x012345'; + const byteArrayShorterThan20Bytes = byteArrayShorterThan32Bytes; + const byteArrayLongerThan32Bytes = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const byteArrayLongerThan32BytesFirstBytesSwapped = + '0x2301456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const byteArrayLongerThan32BytesLastBytesSwapped = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abefcd'; + let testAddress: string; + let testAddressB: string; + const testBytes32 = '0x102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f01020'; + const testBytes32B = '0x534877abd8443578526845cdfef020047528759477fedef87346527659aced32'; + const testUint256 = new BigNumber(testBytes32, 16); + const testUint256B = new BigNumber(testBytes32B, 16); + let shortData: string; + let shortTestBytes: string; + let shortTestBytesAsBuffer: Buffer; + let wordOfData: string; + let wordOfTestBytes: string; + let wordOfTestBytesAsBuffer: Buffer; + let longData: string; + let longTestBytes: string; + let longTestBytesAsBuffer: Buffer; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + // Setup accounts & addresses + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + testAddress = accounts[1]; + testAddressB = accounts[2]; + // Deploy LibBytes + libBytes = await TestLibBytesContract.deployFrom0xArtifactAsync(artifacts.TestLibBytes, provider, txDefaults); + // Verify lengths of test data + const byteArrayShorterThan32BytesLength = ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength; + expect(byteArrayShorterThan32BytesLength).to.be.lessThan(32); + const byteArrayLongerThan32BytesLength = ethUtil.toBuffer(byteArrayLongerThan32Bytes).byteLength; + expect(byteArrayLongerThan32BytesLength).to.be.greaterThan(32); + const testBytes32Length = ethUtil.toBuffer(testBytes32).byteLength; + expect(testBytes32Length).to.be.equal(32); + // Create short test bytes + shortData = '0xffffaa'; + const encodedShortData = ethUtil.toBuffer(shortData); + const shortDataLength = new BigNumber(encodedShortData.byteLength); + const encodedShortDataLength = typeEncodingUtils.encodeUint256(shortDataLength); + shortTestBytesAsBuffer = Buffer.concat([encodedShortDataLength, encodedShortData]); + shortTestBytes = ethUtil.bufferToHex(shortTestBytesAsBuffer); + // Create test bytes one word in length + wordOfData = ethUtil.bufferToHex(typeEncodingUtils.encodeUint256(generatePseudoRandomSalt())); + const encodedWordOfData = ethUtil.toBuffer(wordOfData); + const wordOfDataLength = new BigNumber(encodedWordOfData.byteLength); + const encodedWordOfDataLength = typeEncodingUtils.encodeUint256(wordOfDataLength); + wordOfTestBytesAsBuffer = Buffer.concat([encodedWordOfDataLength, encodedWordOfData]); + wordOfTestBytes = ethUtil.bufferToHex(wordOfTestBytesAsBuffer); + // Create long test bytes (combines short test bytes with word of test bytes) + longData = ethUtil.bufferToHex(Buffer.concat([encodedShortData, encodedWordOfData])); + const longDataLength = new BigNumber(encodedShortData.byteLength + encodedWordOfData.byteLength); + const encodedLongDataLength = typeEncodingUtils.encodeUint256(longDataLength); + longTestBytesAsBuffer = Buffer.concat([encodedLongDataLength, encodedShortData, encodedWordOfData]); + longTestBytes = ethUtil.bufferToHex(longTestBytesAsBuffer); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('popLastByte', () => { + it('should revert if length is 0', async () => { + return expectContractCallFailed( + libBytes.publicPopLastByte.callAsync(constants.NULL_BYTES), + RevertReason.LibBytesGreaterThanZeroLengthRequired, + ); + }); + it('should pop the last byte from the input and return it', async () => { + const [newBytes, poppedByte] = await libBytes.publicPopLastByte.callAsync(byteArrayLongerThan32Bytes); + const expectedNewBytes = byteArrayLongerThan32Bytes.slice(0, -2); + const expectedPoppedByte = `0x${byteArrayLongerThan32Bytes.slice(-2)}`; + expect(newBytes).to.equal(expectedNewBytes); + expect(poppedByte).to.equal(expectedPoppedByte); + }); + }); + + describe('popLast20Bytes', () => { + it('should revert if length is less than 20', async () => { + return expectContractCallFailed( + libBytes.publicPopLast20Bytes.callAsync(byteArrayShorterThan20Bytes), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should pop the last 20 bytes from the input and return it', async () => { + const [newBytes, poppedAddress] = await libBytes.publicPopLast20Bytes.callAsync(byteArrayLongerThan32Bytes); + const expectedNewBytes = byteArrayLongerThan32Bytes.slice(0, -40); + const expectedPoppedAddress = `0x${byteArrayLongerThan32Bytes.slice(-40)}`; + expect(newBytes).to.equal(expectedNewBytes); + expect(poppedAddress).to.equal(expectedPoppedAddress); + }); + }); + + describe('equals', () => { + it('should return true if byte arrays are equal (both arrays < 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayShorterThan32Bytes, + byteArrayShorterThan32Bytes, + ); + return expect(isEqual).to.be.true(); + }); + it('should return true if byte arrays are equal (both arrays > 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.true(); + }); + it('should return false if byte arrays are not equal (first array < 32 bytes, second array > 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayShorterThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (first array > 32 bytes, second array < 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32Bytes, + byteArrayShorterThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (same length, but a byte in first word differs)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32BytesFirstBytesSwapped, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (same length, but a byte in last word differs)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32BytesLastBytesSwapped, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + + describe('should ignore trailing data', () => { + it('should return true when both < 32 bytes', async () => { + const isEqual = await libBytes.publicEqualsPop1.callAsync('0x0102', '0x0103'); + return expect(isEqual).to.be.true(); + }); + }); + }); + + describe('deepCopyBytes', () => { + it('should revert if dest is shorter than source', async () => { + return expectContractCallFailed( + libBytes.publicDeepCopyBytes.callAsync(byteArrayShorterThan32Bytes, byteArrayLongerThan32Bytes), + RevertReason.LibBytesGreaterOrEqualToSourceBytesLengthRequired, + ); + }); + it('should overwrite dest with source if source and dest have equal length', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length - 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(zeroedBytesAfterCopy).to.be.equal(byteArrayLongerThan32Bytes); + }); + it('should overwrite the leftmost len(source) bytes of dest if dest is larger than source', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length * 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + const copiedBytes = zeroedBytesAfterCopy.slice(0, byteArrayLongerThan32Bytes.length); + return expect(copiedBytes).to.be.equal(byteArrayLongerThan32Bytes); + }); + it('should not overwrite the rightmost bytes of dest if dest is larger than source', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length * 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + const expectedNotCopiedBytes = zeroedByteArrayLongerThan32Bytes.slice(byteArrayLongerThan32Bytes.length); + const notCopiedBytes = zeroedBytesAfterCopy.slice(byteArrayLongerThan32Bytes.length); + return expect(notCopiedBytes).to.be.equal(expectedNotCopiedBytes); + }); + }); + + describe('readAddress', () => { + it('should successfully read address when the address takes up the whole array', async () => { + const byteArray = ethUtil.addHexPrefix(testAddress); + const testAddressOffset = new BigNumber(0); + const address = await libBytes.publicReadAddress.callAsync(byteArray, testAddressOffset); + return expect(address).to.be.equal(testAddress); + }); + it('should successfully read address when it is offset in the array', async () => { + const addressByteArrayBuffer = ethUtil.toBuffer(testAddress); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, addressByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testAddressOffset = new BigNumber(prefixByteArrayBuffer.byteLength); + const address = await libBytes.publicReadAddress.callAsync(combinedByteArray, testAddressOffset); + return expect(address).to.be.equal(testAddress); + }); + it('should fail if the byte array is too short to hold an address', async () => { + const shortByteArray = '0xabcdef'; + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadAddress.callAsync(shortByteArray, offset), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { + const byteArray = testAddress; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicReadAddress.callAsync(byteArray, badOffset), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + }); + + describe('writeAddress', () => { + it('should successfully write address when the address takes up the whole array', async () => { + const byteArray = testAddress; + const testAddressOffset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteAddress.callAsync( + byteArray, + testAddressOffset, + testAddressB, + ); + return expect(newByteArray).to.be.equal(testAddressB); + }); + it('should successfully write address when it is offset in the array', async () => { + const addressByteArrayBuffer = ethUtil.toBuffer(testAddress); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, addressByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testAddressOffset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteAddress.callAsync( + combinedByteArray, + testAddressOffset, + testAddressB, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const addressFromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const addressFromOffset = ethUtil.addHexPrefix(ethUtil.bufferToHex(addressFromOffsetBuffer)); + return expect(addressFromOffset).to.be.equal(testAddressB); + }); + it('should fail if the byte array is too short to hold an address', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteAddress.callAsync(byteArrayShorterThan20Bytes, offset, testAddress), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteAddress.callAsync(byteArray, badOffset, testAddress), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + }); + + describe('readBytes32', () => { + it('should successfully read bytes32 when the bytes32 takes up the whole array', async () => { + const testBytes32Offset = new BigNumber(0); + const bytes32 = await libBytes.publicReadBytes32.callAsync(testBytes32, testBytes32Offset); + return expect(bytes32).to.be.equal(testBytes32); + }); + it('should successfully read bytes32 when it is offset in the array)', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testBytes32Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes32 = await libBytes.publicReadBytes32.callAsync(combinedByteArray, testBytes32Offset); + return expect(bytes32).to.be.equal(testBytes32); + }); + it('should fail if the byte array is too short to hold a bytes32', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytes32.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytes32.callAsync(testBytes32, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeBytes32', () => { + it('should successfully write bytes32 when the address takes up the whole array', async () => { + const byteArray = testBytes32; + const testBytes32Offset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteBytes32.callAsync( + byteArray, + testBytes32Offset, + testBytes32B, + ); + return expect(newByteArray).to.be.equal(testBytes32B); + }); + it('should successfully write bytes32 when it is offset in the array', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testBytes32Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteBytes32.callAsync( + combinedByteArray, + testBytes32Offset, + testBytes32B, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const bytes32FromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const bytes32FromOffset = ethUtil.addHexPrefix(ethUtil.bufferToHex(bytes32FromOffsetBuffer)); + return expect(bytes32FromOffset).to.be.equal(testBytes32B); + }); + it('should fail if the byte array is too short to hold a bytes32', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteBytes32.callAsync(byteArrayShorterThan32Bytes, offset, testBytes32), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteBytes32.callAsync(byteArray, badOffset, testBytes32), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('readUint256', () => { + it('should successfully read uint256 when the uint256 takes up the whole array', async () => { + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const byteArray = ethUtil.bufferToHex(testUint256AsBuffer); + const testUint256Offset = new BigNumber(0); + const uint256 = await libBytes.publicReadUint256.callAsync(byteArray, testUint256Offset); + return expect(uint256).to.bignumber.equal(testUint256); + }); + it('should successfully read uint256 when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, testUint256AsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const uint256 = await libBytes.publicReadUint256.callAsync(combinedByteArray, testUint256Offset); + return expect(uint256).to.bignumber.equal(testUint256); + }); + it('should fail if the byte array is too short to hold a uint256', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadUint256.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const byteArray = ethUtil.bufferToHex(testUint256AsBuffer); + const badOffset = new BigNumber(testUint256AsBuffer.byteLength); + return expectContractCallFailed( + libBytes.publicReadUint256.callAsync(byteArray, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeUint256', () => { + it('should successfully write uint256 when the address takes up the whole array', async () => { + const byteArray = testBytes32; + const testUint256Offset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteUint256.callAsync( + byteArray, + testUint256Offset, + testUint256B, + ); + const newByteArrayAsUint256 = new BigNumber(newByteArray, 16); + return expect(newByteArrayAsUint256).to.be.bignumber.equal(testUint256B); + }); + it('should successfully write uint256 when it is offset in the array', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteUint256.callAsync( + combinedByteArray, + testUint256Offset, + testUint256B, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const uint256FromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const uint256FromOffset = new BigNumber( + ethUtil.addHexPrefix(ethUtil.bufferToHex(uint256FromOffsetBuffer)), + 16, + ); + return expect(uint256FromOffset).to.be.bignumber.equal(testUint256B); + }); + it('should fail if the byte array is too short to hold a uint256', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteUint256.callAsync(byteArrayShorterThan32Bytes, offset, testUint256), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteUint256.callAsync(byteArray, badOffset, testUint256), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('readBytes4', () => { + // AssertionError: expected promise to be rejected with an error including 'revert' but it was fulfilled with '0x08c379a0' + it('should revert if byte array has a length < 4', async () => { + const byteArrayLessThan4Bytes = '0x010101'; + return expectContractCallFailed( + libBytes.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, new BigNumber(0)), + RevertReason.LibBytesGreaterOrEqualTo4LengthRequired, + ); + }); + it('should return the first 4 bytes of a byte array of arbitrary length', async () => { + const first4Bytes = await libBytes.publicReadBytes4.callAsync(byteArrayLongerThan32Bytes, new BigNumber(0)); + const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10); + expect(first4Bytes).to.equal(expectedFirst4Bytes); + }); + }); + + describe('readBytesWithLength', () => { + it('should successfully read short, nested array of bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(shortTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully read short, nested array of bytes when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, shortTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully read a nested array of bytes - one word in length - when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(wordOfTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully read a nested array of bytes - one word in length - when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, wordOfTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully read long, nested array of bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(longTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(longData); + }); + it('should successfully read long, nested array of bytes when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, longTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(longData); + }); + it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { + // The length of the nested array is 32 bytes. By storing less than 32 bytes, a length cannot be read. + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if we store a nested byte array length, without a nested byte array', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(testBytes32, offset), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the nested byte array', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(testBytes32, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeBytesWithLength', () => { + it('should successfully write short, nested array of bytes when it takes up the whole array)', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + shortData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(shortData); + }); + it('should successfully write short, nested array of bytes when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + shortTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + bytesWritten, + testBytesOffset, + shortData, + ); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully write a nested array of bytes - one word in length - when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(wordOfTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + wordOfData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(wordOfData); + }); + it('should successfully write a nested array of bytes - one word in length - when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + wordOfTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + bytesWritten, + testBytesOffset, + wordOfData, + ); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully write a long, nested bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(longTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + longData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(longData); + }); + it('should successfully write long, nested array of bytes when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + longTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(bytesWritten, testBytesOffset, longData); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(longData); + }); + it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { + const offset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(1)); + return expectContractCallFailed( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, offset, longData), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array)', async () => { + const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); + const badOffset = new BigNumber(ethUtil.toBuffer(shortTestBytesAsBuffer).byteLength); + return expectContractCallFailed( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, badOffset, shortData), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + }); + + describe('memCopy', () => { + // Create memory 0x000102...FF + const memSize = 256; + // tslint:disable:no-shadowed-variable + const memory = new Uint8Array(memSize).map((_, i) => i); + const memHex = toHex(memory); + + // Reference implementation to test against + const refMemcpy = (mem: Uint8Array, dest: number, source: number, length: number): Uint8Array => + Uint8Array.from(mem).copyWithin(dest, source, source + length); + + // Test vectors: destination, source, length, job description + type Tests = Array<[number, number, number, string]>; + + const test = (tests: Tests) => + tests.forEach(([dest, source, length, job]) => + it(job, async () => { + const expected = refMemcpy(memory, dest, source, length); + const resultStr = await libBytes.testMemcpy.callAsync( + memHex, + new BigNumber(dest), + new BigNumber(source), + new BigNumber(length), + ); + const result = fromHex(resultStr); + expect(result).to.deep.equal(expected); + }), + ); + + test([[0, 0, 0, 'copies zero bytes with overlap']]); + + describe('copies forward', () => + test([ + [128, 0, 0, 'zero bytes'], + [128, 0, 1, 'one byte'], + [128, 0, 11, 'eleven bytes'], + [128, 0, 31, 'thirty-one bytes'], + [128, 0, 32, 'one word'], + [128, 0, 64, 'two words'], + [128, 0, 96, 'three words'], + [128, 0, 33, 'one word and one byte'], + [128, 0, 72, 'two words and eight bytes'], + [128, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word', () => + test([ + [16, 0, 0, 'zero bytes'], + [16, 0, 1, 'one byte'], + [16, 0, 11, 'eleven bytes'], + [16, 0, 16, 'sixteen bytes'], + ])); + + describe('copies forward with one byte overlap', () => + test([ + [0, 0, 1, 'one byte'], + [10, 0, 11, 'eleven bytes'], + [30, 0, 31, 'thirty-one bytes'], + [31, 0, 32, 'one word'], + [32, 0, 33, 'one word and one byte'], + [71, 0, 72, 'two words and eight bytes'], + [99, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with thirty-one bytes overlap', () => + test([ + [0, 0, 31, 'thirty-one bytes'], + [1, 0, 32, 'one word'], + [2, 0, 33, 'one word and one byte'], + [41, 0, 72, 'two words and eight bytes'], + [69, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with one word overlap', () => + test([ + [0, 0, 32, 'one word'], + [1, 0, 33, 'one word and one byte'], + [41, 0, 72, 'two words and eight bytes'], + [69, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with one word and one byte overlap', () => + test([ + [0, 0, 33, 'one word and one byte'], + [40, 0, 72, 'two words and eight bytes'], + [68, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with two words overlap', () => + test([ + [0, 0, 64, 'two words'], + [8, 0, 72, 'two words and eight bytes'], + [36, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word and one byte overlap', () => + test([[0, 0, 1, 'one byte'], [10, 0, 11, 'eleven bytes'], [15, 0, 16, 'sixteen bytes']])); + + describe('copies backward', () => + test([ + [0, 128, 0, 'zero bytes'], + [0, 128, 1, 'one byte'], + [0, 128, 11, 'eleven bytes'], + [0, 128, 31, 'thirty-one bytes'], + [0, 128, 32, 'one word'], + [0, 128, 64, 'two words'], + [0, 128, 96, 'three words'], + [0, 128, 33, 'one word and one byte'], + [0, 128, 72, 'two words and eight bytes'], + [0, 128, 100, 'three words and four bytes'], + ])); + + describe('copies backward within one word', () => + test([ + [0, 16, 0, 'zero bytes'], + [0, 16, 1, 'one byte'], + [0, 16, 11, 'eleven bytes'], + [0, 16, 16, 'sixteen bytes'], + ])); + + describe('copies backward with one byte overlap', () => + test([ + [0, 0, 1, 'one byte'], + [0, 10, 11, 'eleven bytes'], + [0, 30, 31, 'thirty-one bytes'], + [0, 31, 32, 'one word'], + [0, 32, 33, 'one word and one byte'], + [0, 71, 72, 'two words and eight bytes'], + [0, 99, 100, 'three words and four bytes'], + ])); + + describe('copies backward with thirty-one bytes overlap', () => + test([ + [0, 0, 31, 'thirty-one bytes'], + [0, 1, 32, 'one word'], + [0, 2, 33, 'one word and one byte'], + [0, 41, 72, 'two words and eight bytes'], + [0, 69, 100, 'three words and four bytes'], + ])); + + describe('copies backward with one word overlap', () => + test([ + [0, 0, 32, 'one word'], + [0, 1, 33, 'one word and one byte'], + [0, 41, 72, 'two words and eight bytes'], + [0, 69, 100, 'three words and four bytes'], + ])); + + describe('copies backward with one word and one byte overlap', () => + test([ + [0, 0, 33, 'one word and one byte'], + [0, 40, 72, 'two words and eight bytes'], + [0, 68, 100, 'three words and four bytes'], + ])); + + describe('copies backward with two words overlap', () => + test([ + [0, 0, 64, 'two words'], + [0, 8, 72, 'two words and eight bytes'], + [0, 36, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word and one byte overlap', () => + test([[0, 0, 1, 'one byte'], [0, 10, 11, 'eleven bytes'], [0, 15, 16, 'sixteen bytes']])); + }); +}); +// tslint:disable:max-file-line-count diff --git a/packages/contracts/test/multi_sig_with_time_lock.ts b/packages/contracts/test/multi_sig_with_time_lock.ts deleted file mode 100644 index d35efe9da..000000000 --- a/packages/contracts/test/multi_sig_with_time_lock.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { LogWithDecodedArgs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, web3Factory } from '@0xproject/dev-utils'; -import { AbiDecoder, BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import * as multiSigWalletJSON from '../../build/contracts/MultiSigWalletWithTimeLock.json'; -import { MultiSigWalletContract } from '../src/contract_wrappers/generated/multi_sig_wallet'; -import { MultiSigWalletWithTimeLockContract } from '../src/contract_wrappers/generated/multi_sig_wallet_with_time_lock'; -import { artifacts } from '../util/artifacts'; -import { constants } from '../util/constants'; -import { MultiSigWrapper } from '../util/multi_sig_wrapper'; -import { ContractName, SubmissionContractEventArgs } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -const MULTI_SIG_ABI = artifacts.MultiSigWalletWithTimeLockArtifact.networks[constants.TESTRPC_NETWORK_ID].abi; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); -const abiDecoder = new AbiDecoder([MULTI_SIG_ABI]); - -describe('MultiSigWalletWithTimeLock', () => { - let owners: string[]; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owners = [accounts[0], accounts[1]]; - }); - const SIGNATURES_REQUIRED = new BigNumber(2); - const SECONDS_TIME_LOCKED = new BigNumber(10000); - - let multiSig: MultiSigWalletWithTimeLockContract; - let multiSigWrapper: MultiSigWrapper; - let txId: BigNumber; - let initialSecondsTimeLocked: number; - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('changeTimeLock', () => { - describe('initially non-time-locked', async () => { - before('deploy a walet', async () => { - const multiSigInstance = await deployer.deployAsync(ContractName.MultiSigWalletWithTimeLock, [ - owners, - SIGNATURES_REQUIRED, - 0, - ]); - multiSig = new MultiSigWalletWithTimeLockContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - - const secondsTimeLocked = await multiSig.secondsTimeLocked.callAsync(); - initialSecondsTimeLocked = secondsTimeLocked.toNumber(); - }); - it('should throw when not called by wallet', async () => { - return expect( - multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw without enough confirmations', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - return expect( - multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should set confirmation time with enough confirmations', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(res.logs).to.have.length(2); - - const blockNum = await web3Wrapper.getBlockNumberAsync(); - const blockInfo = await web3Wrapper.getBlockAsync(blockNum); - const timestamp = new BigNumber(blockInfo.timestamp); - const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId)); - - expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum); - }); - - it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - - expect(initialSecondsTimeLocked).to.be.equal(0); - - txHash = await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(res.logs).to.have.length(2); - - const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); - expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED); - }); - }); - describe('initially time-locked', async () => { - before('deploy a walet', async () => { - const multiSigInstance = await deployer.deployAsync(ContractName.MultiSigWalletWithTimeLock, [ - owners, - SIGNATURES_REQUIRED, - SECONDS_TIME_LOCKED, - ]); - multiSig = new MultiSigWalletWithTimeLockContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - - const secondsTimeLocked = await multiSig.secondsTimeLocked.callAsync(); - initialSecondsTimeLocked = secondsTimeLocked.toNumber(); - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [newSecondsTimeLocked], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { - from: owners[1], - }); - const confRes = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(confRes.logs).to.have.length(2); - }); - const newSecondsTimeLocked = 0; - it('should throw if it has enough confirmations but is not past the time lock', async () => { - return expect( - multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should execute if it has enough confirmations and is past the time lock', async () => { - await web3Wrapper.increaseTimeAsync(SECONDS_TIME_LOCKED.toNumber()); - await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }); - - const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); - expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked); - }); - }); - }); -}); diff --git a/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts b/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts deleted file mode 100644 index 6d20a67f3..000000000 --- a/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { LogWithDecodedArgs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { AbiDecoder } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { MultiSigWalletContract } from '../src/contract_wrappers/generated/multi_sig_wallet'; -import { MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract } from '../src/contract_wrappers/generated/multi_sig_wallet_with_time_lock_except_remove_authorized_address'; -import { TokenTransferProxyContract } from '../src/contract_wrappers/generated/token_transfer_proxy'; -import { artifacts } from '../util/artifacts'; -import { constants } from '../util/constants'; -import { crypto } from '../util/crypto'; -import { MultiSigWrapper } from '../util/multi_sig_wrapper'; -import { ContractName, SubmissionContractEventArgs, TransactionDataParams } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; -const PROXY_ABI = artifacts.TokenTransferProxyArtifact.networks[constants.TESTRPC_NETWORK_ID].abi; -const MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI = - artifacts.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact.networks[constants.TESTRPC_NETWORK_ID] - .abi; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const abiDecoder = new AbiDecoder([MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI]); - -describe('MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', () => { - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - let owners: string[]; - const requiredApprovals = 2; - const SECONDS_TIME_LOCKED = 1000000; - - // initialize fake addresses - let authorizedAddress: string; - let unauthorizedAddress: string; - - let tokenTransferProxy: TokenTransferProxyContract; - let multiSig: MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract; - let multiSigWrapper: MultiSigWrapper; - - let validDestination: string; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owners = [accounts[0], accounts[1]]; - [authorizedAddress, unauthorizedAddress] = accounts; - const initialOwner = accounts[0]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(authorizedAddress, { - from: initialOwner, - }); - const multiSigInstance = await deployer.deployAsync( - ContractName.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress, - [owners, requiredApprovals, SECONDS_TIME_LOCKED, tokenTransferProxy.address], - ); - multiSig = new MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - await tokenTransferProxy.transferOwnership.sendTransactionAsync(multiSig.address, { - from: initialOwner, - }); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - validDestination = tokenTransferProxy.address; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('isFunctionRemoveAuthorizedAddress', () => { - it('should throw if data is not for removeAuthorizedAddress', async () => { - const data = MultiSigWrapper.encodeFnArgs('addAuthorizedAddress', PROXY_ABI, [owners[0]]); - return expect(multiSig.isFunctionRemoveAuthorizedAddress.callAsync(data)).to.be.rejectedWith( - constants.REVERT, - ); - }); - - it('should return true if data is for removeAuthorizedAddress', async () => { - const data = MultiSigWrapper.encodeFnArgs('removeAuthorizedAddress', PROXY_ABI, [owners[0]]); - const isFunctionRemoveAuthorizedAddress = await multiSig.isFunctionRemoveAuthorizedAddress.callAsync(data); - expect(isFunctionRemoveAuthorizedAddress).to.be.true(); - }); - }); - - describe('executeRemoveAuthorizedAddress', () => { - it('should throw without the required confirmations', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if tx destination is not the tokenTransferProxy', async () => { - const invalidTokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const invalidDestination = invalidTokenTransferProxy.address; - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(invalidDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if tx data is not for removeAuthorizedAddress', async () => { - const dataParams: TransactionDataParams = { - name: 'addAuthorizedAddress', - abi: PROXY_ABI, - args: [unauthorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should execute removeAuthorizedAddress for valid tokenTransferProxy if fully confirmed', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - await multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(authorizedAddress); - expect(isAuthorized).to.be.false(); - }); - - it('should throw if already executed', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - await multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }); - const tx = await multiSig.transactions.callAsync(txId); - const isExecuted = tx[3]; - expect(isExecuted).to.be.true(); - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); -}); diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts new file mode 100644 index 000000000..6b98605d3 --- /dev/null +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -0,0 +1,468 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + AssetProxyOwnerAssetProxyRegistrationEventArgs, + AssetProxyOwnerContract, + AssetProxyOwnerExecutionEventArgs, + AssetProxyOwnerExecutionFailureEventArgs, + AssetProxyOwnerSubmissionEventArgs, +} from '../../generated_contract_wrappers/asset_proxy_owner'; +import { MixinAuthorizableContract } from '../../generated_contract_wrappers/mixin_authorizable'; +import { TestAssetProxyOwnerContract } from '../../generated_contract_wrappers/test_asset_proxy_owner'; +import { artifacts } from '../utils/artifacts'; +import { + expectContractCallFailedWithoutReasonAsync, + expectContractCreationFailedWithoutReason, + expectTransactionFailedWithoutReasonAsync, +} from '../utils/assertions'; +import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { MultiSigWrapper } from '../utils/multi_sig_wrapper'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('AssetProxyOwner', () => { + let owners: string[]; + let authorized: string; + const REQUIRED_APPROVALS = new BigNumber(2); + const SECONDS_TIME_LOCKED = new BigNumber(1000000); + + let erc20Proxy: MixinAuthorizableContract; + let erc721Proxy: MixinAuthorizableContract; + let testAssetProxyOwner: TestAssetProxyOwnerContract; + let multiSigWrapper: MultiSigWrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owners = [accounts[0], accounts[1]]; + const initialOwner = (authorized = accounts[0]); + erc20Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync( + artifacts.MixinAuthorizable, + provider, + txDefaults, + ); + erc721Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync( + artifacts.MixinAuthorizable, + provider, + txDefaults, + ); + const defaultAssetProxyContractAddresses: string[] = []; + testAssetProxyOwner = await TestAssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.TestAssetProxyOwner, + provider, + txDefaults, + owners, + defaultAssetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + multiSigWrapper = new MultiSigWrapper(testAssetProxyOwner, provider); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, { + from: initialOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, { + from: initialOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('constructor', () => { + it('should register passed in assetProxyContracts', async () => { + const assetProxyContractAddresses = [erc20Proxy.address, erc721Proxy.address]; + const newMultiSig = await AssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.AssetProxyOwner, + provider, + txDefaults, + owners, + assetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + const isErc20ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc20Proxy.address); + const isErc721ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc721Proxy.address); + expect(isErc20ProxyRegistered).to.equal(true); + expect(isErc721ProxyRegistered).to.equal(true); + }); + it('should throw if a null address is included in assetProxyContracts', async () => { + const assetProxyContractAddresses = [erc20Proxy.address, constants.NULL_ADDRESS]; + return expectContractCreationFailedWithoutReason( + AssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.AssetProxyOwner, + provider, + txDefaults, + owners, + assetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ), + ); + }); + }); + + describe('isFunctionRemoveAuthorizedAddressAtIndex', () => { + it('should return false if data is not for removeAuthorizedAddressAtIndex', async () => { + const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + owners[0], + ); + + const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync( + notRemoveAuthorizedAddressData, + ); + expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.false(); + }); + + it('should return true if data is for removeAuthorizedAddressAtIndex', async () => { + const index = new BigNumber(0); + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + owners[0], + index, + ); + const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync( + removeAuthorizedAddressAtIndexData, + ); + expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.true(); + }); + }); + + describe('readBytes4', () => { + it('should revert if byte array has a length < 4', async () => { + const byteArrayLessThan4Bytes = '0x010101'; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, new BigNumber(0)), + ); + }); + it('should return the first 4 bytes of a byte array of arbitrary length', async () => { + const byteArrayLongerThan32Bytes = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const first4Bytes = await testAssetProxyOwner.publicReadBytes4.callAsync( + byteArrayLongerThan32Bytes, + new BigNumber(0), + ); + const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10); + expect(first4Bytes).to.equal(expectedFirst4Bytes); + }); + }); + + describe('registerAssetProxy', () => { + it('should throw if not called by multisig', async () => { + const isRegistered = true; + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, { + from: owners[0], + }), + ); + }); + + it('should register an address if called by multisig after timelock', async () => { + const addressToRegister = erc20Proxy.address; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await multiSigWrapper.executeTransactionAsync(txId, owners[0]); + const registerLog = executeTxRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerAssetProxyRegistrationEventArgs + >; + expect(registerLog.args.assetProxyContract).to.equal(addressToRegister); + expect(registerLog.args.isRegistered).to.equal(isRegistered); + + const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync( + addressToRegister, + ); + expect(isAssetProxyRegistered).to.equal(isRegistered); + }); + + it('should fail if registering a null address', async () => { + const addressToRegister = constants.NULL_ADDRESS; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await multiSigWrapper.executeTransactionAsync(txId, owners[0]); + const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionFailureEventArgs>; + expect(failureLog.args.transactionId).to.be.bignumber.equal(txId); + + const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync( + addressToRegister, + ); + expect(isAssetProxyRegistered).to.equal(false); + }); + }); + + describe('Calling removeAuthorizedAddressAtIndex', () => { + const erc20Index = new BigNumber(0); + const erc721Index = new BigNumber(1); + before('authorize both proxies and register erc20 proxy', async () => { + // Only register ERC20 proxy + const addressToRegister = erc20Proxy.address; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const registerAssetProxySubmitRes = await multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const registerAssetProxySubmitLog = registerAssetProxySubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(authorized); + const erc20AddAuthorizedAddressSubmitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc721AddAuthorizedAddressSubmitRes = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc20AddAuthorizedAddressSubmitLog = erc20AddAuthorizedAddressSubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + const erc721AddAuthorizedAddressSubmitLog = erc721AddAuthorizedAddressSubmitRes + .logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + + const registerAssetProxyTxId = registerAssetProxySubmitLog.args.transactionId; + const erc20AddAuthorizedAddressTxId = erc20AddAuthorizedAddressSubmitLog.args.transactionId; + const erc721AddAuthorizedAddressTxId = erc721AddAuthorizedAddressSubmitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(registerAssetProxyTxId, owners[1]); + await multiSigWrapper.confirmTransactionAsync(erc20AddAuthorizedAddressTxId, owners[1]); + await multiSigWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + await multiSigWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]); + await multiSigWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0]); + await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0]); + }); + + describe('validRemoveAuthorizedAddressAtIndexTx', () => { + it('should revert if data is not for removeAuthorizedAddressAtIndex and proxy is registered', async () => { + const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + authorized, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + notRemoveAuthorizedAddressData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + ); + }); + + it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync( + txId, + ); + expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true(); + }); + + it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + ); + }); + }); + + describe('executeRemoveAuthorizedAddressAtIndex', () => { + it('should throw without the required confirmations', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should throw if tx destination is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => { + const newAuthorized = owners[1]; + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + newAuthorized, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); + const execLog = execRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + expect(execLog.args.transactionId).to.be.bignumber.equal(txId); + + const tx = await testAssetProxyOwner.transactions.callAsync(txId); + const isExecuted = tx[3]; + expect(isExecuted).to.equal(true); + + const isAuthorized = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorized).to.equal(false); + }); + + it('should throw if already executed', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); + const execLog = execRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + expect(execLog.args.transactionId).to.be.bignumber.equal(txId); + + const tx = await testAssetProxyOwner.transactions.callAsync(txId); + const isExecuted = tx[3]; + expect(isExecuted).to.equal(true); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts new file mode 100644 index 000000000..8eeeeca6b --- /dev/null +++ b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts @@ -0,0 +1,168 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + MultiSigWalletWithTimeLockContract, + MultiSigWalletWithTimeLockSubmissionEventArgs, +} from '../../generated_contract_wrappers/multi_sig_wallet_with_time_lock'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { MultiSigWrapper } from '../utils/multi_sig_wrapper'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('MultiSigWalletWithTimeLock', () => { + let owners: string[]; + const REQUIRED_APPROVALS = new BigNumber(2); + const SECONDS_TIME_LOCKED = new BigNumber(1000000); + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owners = [accounts[0], accounts[1]]; + }); + + let multiSig: MultiSigWalletWithTimeLockContract; + let multiSigWrapper: MultiSigWrapper; + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('changeTimeLock', () => { + describe('initially non-time-locked', async () => { + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before('deploy a wallet', async () => { + const secondsTimeLocked = new BigNumber(0); + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + secondsTimeLocked, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + }); + + it('should throw when not called by wallet', async () => { + return expectTransactionFailedWithoutReasonAsync( + multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }), + ); + }); + + it('should throw without enough confirmations', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const res = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectTransactionFailedWithoutReasonAsync( + multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + ); + }); + + it('should set confirmation time with enough confirmations', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = subLog.args.transactionId; + + const confirmRes = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + expect(confirmRes.logs).to.have.length(2); + + const blockNum = await web3Wrapper.getBlockNumberAsync(); + const blockInfo = await web3Wrapper.getBlockAsync(blockNum); + const timestamp = new BigNumber(blockInfo.timestamp); + const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId)); + + expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum); + }); + + it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = subLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await multiSigWrapper.executeTransactionAsync(txId, owners[1]); + + const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); + expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED); + }); + }); + describe('initially time-locked', async () => { + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + let txId: BigNumber; + const newSecondsTimeLocked = new BigNumber(0); + before('deploy a wallet, submit transaction to change timelock, and confirm the transaction', async () => { + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(newSecondsTimeLocked); + const res = await multiSigWrapper.submitTransactionAsync( + multiSig.address, + changeTimeLockData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + txId = log.args.transactionId; + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + }); + + it('should throw if it has enough confirmations but is not past the time lock', async () => { + return expectTransactionFailedWithoutReasonAsync( + multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + ); + }); + + it('should execute if it has enough confirmations and is past the time lock', async () => { + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); + expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked); + }); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/token_registry.ts b/packages/contracts/test/token_registry.ts index 9dcc77b82..7cc43be9b 100644 --- a/packages/contracts/test/token_registry.ts +++ b/packages/contracts/test/token_registry.ts @@ -1,20 +1,17 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber, NULL_BYTES } from '@0xproject/utils'; import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); import * as _ from 'lodash'; -import * as Web3 from 'web3'; -import { TokenRegistryContract } from '../src/contract_wrappers/generated/token_registry'; -import { constants } from '../util/constants'; -import { TokenRegWrapper } from '../util/token_registry_wrapper'; -import { ContractName } from '../util/types'; +import { TokenRegistryContract } from '../generated_contract_wrappers/token_registry'; +import { artifacts } from './utils/artifacts'; +import { expectTransactionFailedWithoutReasonAsync } from './utils/assertions'; import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; +import { constants } from './utils/constants'; +import { TokenRegWrapper } from './utils/token_registry_wrapper'; +import { provider, txDefaults, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; @@ -25,13 +22,19 @@ describe('TokenRegistry', () => { let notOwner: string; let tokenReg: TokenRegistryContract; let tokenRegWrapper: TokenRegWrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); owner = accounts[0]; notOwner = accounts[1]; - const tokenRegInstance = await deployer.deployAsync(ContractName.TokenRegistry); - tokenReg = new TokenRegistryContract(web3Wrapper, tokenRegInstance.abi, tokenRegInstance.address); - tokenRegWrapper = new TokenRegWrapper(tokenReg); + tokenReg = await TokenRegistryContract.deployFrom0xArtifactAsync(artifacts.TokenRegistry, provider, txDefaults); + tokenRegWrapper = new TokenRegWrapper(tokenReg, provider); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -62,17 +65,17 @@ describe('TokenRegistry', () => { }; const nullToken = { - address: ZeroEx.NULL_ADDRESS, + address: constants.NULL_ADDRESS, name: '', symbol: '', decimals: 0, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, + ipfsHash: NULL_BYTES, + swarmHash: NULL_BYTES, }; describe('addToken', () => { it('should throw when not called by owner', async () => { - return expect(tokenRegWrapper.addTokenAsync(token1, notOwner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(token1, notOwner)); }); it('should add token metadata when called by owner', async () => { @@ -84,20 +87,18 @@ describe('TokenRegistry', () => { it('should throw if token already exists', async () => { await tokenRegWrapper.addTokenAsync(token1, owner); - return expect(tokenRegWrapper.addTokenAsync(token1, owner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(token1, owner)); }); it('should throw if token address is null', async () => { - return expect(tokenRegWrapper.addTokenAsync(nullToken, owner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(nullToken, owner)); }); it('should throw if name already exists', async () => { await tokenRegWrapper.addTokenAsync(token1, owner); const duplicateNameToken = _.assign({}, token2, { name: token1.name }); - return expect(tokenRegWrapper.addTokenAsync(duplicateNameToken, owner)).to.be.rejectedWith( - constants.REVERT, - ); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(duplicateNameToken, owner)); }); it('should throw if symbol already exists', async () => { @@ -106,8 +107,8 @@ describe('TokenRegistry', () => { symbol: token1.symbol, }); - return expect(tokenRegWrapper.addTokenAsync(duplicateSymbolToken, owner)).to.be.rejectedWith( - constants.REVERT, + return expectTransactionFailedWithoutReasonAsync( + tokenRegWrapper.addTokenAsync(duplicateSymbolToken, owner), ); }); }); @@ -133,15 +134,18 @@ describe('TokenRegistry', () => { describe('setTokenName', () => { it('should throw when not called by owner', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should change the token name when called by owner', async () => { - await tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { - from: owner, - }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const [newData, oldData] = await Promise.all([ tokenRegWrapper.getTokenByNameAsync(token2.name), tokenRegWrapper.getTokenByNameAsync(token1.name), @@ -156,29 +160,32 @@ describe('TokenRegistry', () => { it('should throw if the name already exists', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token does not exist', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(nullToken.address, token2.name, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); describe('setTokenSymbol', () => { it('should throw when not called by owner', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: notOwner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should change the token symbol when called by owner', async () => { - await tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const [newData, oldData] = await Promise.all([ tokenRegWrapper.getTokenBySymbolAsync(token2.symbol), tokenRegWrapper.getTokenBySymbolAsync(token1.symbol), @@ -193,52 +200,55 @@ describe('TokenRegistry', () => { it('should throw if the symbol already exists', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token does not exist', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(nullToken.address, token2.symbol, { from: owner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); describe('removeToken', () => { it('should throw if not called by owner', async () => { const index = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(token1.address, index, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should remove token metadata when called by owner', async () => { const index = new BigNumber(0); - await tokenReg.removeToken.sendTransactionAsync(token1.address, index, { - from: owner, - }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.removeToken.sendTransactionAsync(token1.address, index, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const tokenData = await tokenRegWrapper.getTokenMetaDataAsync(token1.address); expect(tokenData).to.be.deep.equal(nullToken); }); it('should throw if token does not exist', async () => { const index = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(nullToken.address, index, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token at given index does not match address', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); const incorrectIndex = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(token2.address, incorrectIndex, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); }); diff --git a/packages/contracts/test/token_transfer_proxy/auth.ts b/packages/contracts/test/token_transfer_proxy/auth.ts deleted file mode 100644 index a1ccc6ff7..000000000 --- a/packages/contracts/test/token_transfer_proxy/auth.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { constants } from '../../util/constants'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('TokenTransferProxy', () => { - let owner: string; - let notOwner: string; - let address: string; - let tokenTransferProxy: TokenTransferProxyContract; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = address = accounts[0]; - notOwner = accounts[1]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('addAuthorizedAddress', () => { - it('should throw if not called by owner', async () => { - return expect( - tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); - }); - it('should allow owner to add an authorized address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(address); - expect(isAuthorized).to.be.true(); - }); - it('should throw if owner attempts to authorize a duplicate address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - return expect( - tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); - - describe('removeAuthorizedAddress', () => { - it('should throw if not called by owner', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - return expect( - tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: notOwner, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should allow owner to remove an authorized address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - await tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(address); - expect(isAuthorized).to.be.false(); - }); - - it('should throw if owner attempts to remove an address that is not authorized', async () => { - return expect( - tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); - - describe('getAuthorizedAddresses', () => { - it('should return all authorized addresses', async () => { - const initial = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(initial).to.have.length(0); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const afterAdd = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(afterAdd).to.have.length(1); - expect(afterAdd).to.include(address); - - await tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const afterRemove = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(afterRemove).to.have.length(0); - }); - }); -}); diff --git a/packages/contracts/test/token_transfer_proxy/transfer_from.ts b/packages/contracts/test/token_transfer_proxy/transfer_from.ts deleted file mode 100644 index 415d068be..000000000 --- a/packages/contracts/test/token_transfer_proxy/transfer_from.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('TokenTransferProxy', () => { - let accounts: string[]; - let owner: string; - let notAuthorized: string; - const INIT_BAL = new BigNumber(100000000); - const INIT_ALLOW = new BigNumber(100000000); - - let tokenTransferProxy: TokenTransferProxyContract; - let rep: DummyTokenContract; - let dmyBalances: Balances; - - before(async () => { - accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = notAuthorized = accounts[0]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - const repInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - - dmyBalances = new Balances([rep], [accounts[0], accounts[1]]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { - from: accounts[0], - }), - rep.setBalance.sendTransactionAsync(accounts[0], INIT_BAL, { from: owner }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { - from: accounts[1], - }), - rep.setBalance.sendTransactionAsync(accounts[1], INIT_BAL, { from: owner }), - ]); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('transferFrom', () => { - it('should throw when called by an unauthorized address', async () => { - expect( - tokenTransferProxy.transferFrom.sendTransactionAsync( - rep.address, - accounts[0], - accounts[1], - new BigNumber(1000), - { - from: notAuthorized, - }, - ), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should allow an authorized address to transfer', async () => { - const balances = await dmyBalances.getAsync(); - - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(notAuthorized, { - from: owner, - }); - const transferAmt = new BigNumber(10000); - await tokenTransferProxy.transferFrom.sendTransactionAsync( - rep.address, - accounts[0], - accounts[1], - transferAmt, - { - from: notAuthorized, - }, - ); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances[accounts[0]][rep.address]).to.be.bignumber.equal( - balances[accounts[0]][rep.address].minus(transferAmt), - ); - expect(newBalances[accounts[1]][rep.address]).to.be.bignumber.equal( - balances[accounts[1]][rep.address].add(transferAmt), - ); - }); - }); -}); diff --git a/packages/contracts/test/ether_token.ts b/packages/contracts/test/tokens/ether_token.ts index 6c7354c2b..a104fc915 100644 --- a/packages/contracts/test/ether_token.ts +++ b/packages/contracts/test/tokens/ether_token.ts @@ -1,15 +1,14 @@ -import { ZeroEx, ZeroExError } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber, promisify } from '@0xproject/utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; +import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; +import { artifacts } from '../utils/artifacts'; +import { expectInsufficientFundsAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; @@ -17,18 +16,22 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('EtherToken', () => { let account: string; - const gasPrice = ZeroEx.toBaseUnitAmount(new BigNumber(20), 9); - let zeroEx: ZeroEx; - let etherTokenAddress: string; + const gasPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 9); + let etherToken: WETH9Contract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); account = accounts[0]; - const etherToken = await deployer.deployAsync(ContractName.EtherToken); - etherTokenAddress = etherToken.address; - zeroEx = new ZeroEx(web3.currentProvider, { + etherToken = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, { gasPrice, - networkId: constants.TESTRPC_NETWORK_ID, + ...txDefaults, }); }); beforeEach(async () => { @@ -42,23 +45,24 @@ describe('EtherToken', () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); const ethToDeposit = initEthBalance.plus(1); - return expect(zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account)).to.be.rejectedWith( - ZeroExError.InsufficientEthBalanceForDeposit, - ); + return expectInsufficientFundsAsync(etherToken.deposit.sendTransactionAsync({ value: ethToDeposit })); }); it('should convert deposited Ether to wrapped Ether tokens', async () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); - const ethToDeposit = new BigNumber(web3.toWei(1, 'ether')); + const ethToDeposit = new BigNumber(Web3Wrapper.toWei(new BigNumber(1))); - const txHash = await zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const txHash = await etherToken.deposit.sendTransactionAsync({ value: ethToDeposit }); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal(initEthBalance.minus(ethToDeposit.plus(ethSpentOnGas))); expect(finalEthTokenBalance).to.be.bignumber.equal(initEthTokenBalance.plus(ethToDeposit)); @@ -67,29 +71,35 @@ describe('EtherToken', () => { describe('withdraw', () => { it('should throw if caller attempts to withdraw greater than caller balance', async () => { - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); const ethTokensToWithdraw = initEthTokenBalance.plus(1); - return expect( - zeroEx.etherToken.withdrawAsync(etherTokenAddress, ethTokensToWithdraw, account), - ).to.be.rejectedWith(ZeroExError.InsufficientWEthBalanceForWithdrawal); + return expectTransactionFailedWithoutReasonAsync( + etherToken.withdraw.sendTransactionAsync(ethTokensToWithdraw), + ); }); it('should convert ether tokens to ether with sufficient balance', async () => { - const ethToDeposit = new BigNumber(web3.toWei(1, 'ether')); - await zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const ethToDeposit = new BigNumber(Web3Wrapper.toWei(new BigNumber(1))); + await web3Wrapper.awaitTransactionSuccessAsync( + await etherToken.deposit.sendTransactionAsync({ value: ethToDeposit }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); const ethTokensToWithdraw = initEthTokenBalance; expect(ethTokensToWithdraw).to.not.be.bignumber.equal(0); - const txHash = await zeroEx.etherToken.withdrawAsync(etherTokenAddress, ethTokensToWithdraw, account, { - gasLimit: constants.MAX_ETHERTOKEN_WITHDRAW_GAS, + const txHash = await etherToken.withdraw.sendTransactionAsync(ethTokensToWithdraw, { + gas: constants.MAX_ETHERTOKEN_WITHDRAW_GAS, }); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal( initEthBalance.plus(ethTokensToWithdraw.minus(ethSpentOnGas)), @@ -101,22 +111,25 @@ describe('EtherToken', () => { describe('fallback', () => { it('should convert sent ether to ether tokens', async () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); - const ethToDeposit = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + const ethToDeposit = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18); const txHash = await web3Wrapper.sendTransactionAsync({ from: account, - to: etherTokenAddress, + to: etherToken.address, value: ethToDeposit, gasPrice, }); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal(initEthBalance.minus(ethToDeposit.plus(ethSpentOnGas))); expect(finalEthTokenBalance).to.be.bignumber.equal(initEthTokenBalance.plus(ethToDeposit)); diff --git a/packages/contracts/test/tokens/unlimited_allowance_token.ts b/packages/contracts/test/tokens/unlimited_allowance_token.ts new file mode 100644 index 000000000..81d931fc5 --- /dev/null +++ b/packages/contracts/test/tokens/unlimited_allowance_token.ts @@ -0,0 +1,191 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('UnlimitedAllowanceToken', () => { + let owner: string; + let spender: string; + const MAX_MINT_VALUE = new BigNumber(100000000000000000000); + let token: DummyERC20TokenContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = accounts[0]; + spender = accounts[1]; + token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.mint.sendTransactionAsync(MAX_MINT_VALUE, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('transfer', () => { + it('should throw if owner has insufficient balance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + return expectContractCallFailed( + token.transfer.callAsync(spender, amountToTransfer, { from: owner }), + RevertReason.Erc20InsufficientBalance, + ); + }); + + it('should transfer balance from sender to receiver', async () => { + const receiver = spender; + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transfer.sendTransactionAsync(receiver, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const finalOwnerBalance = await token.balanceOf.callAsync(owner); + const finalReceiverBalance = await token.balanceOf.callAsync(receiver); + + const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); + const expectedFinalReceiverBalance = amountToTransfer; + expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); + expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); + }); + + it('should return true on a 0 value transfer', async () => { + const didReturnTrue = await token.transfer.callAsync(spender, new BigNumber(0), { + from: owner, + }); + expect(didReturnTrue).to.be.true(); + }); + }); + + describe('transferFrom', () => { + it('should throw if owner has insufficient balance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return expectContractCallFailed( + token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }), + RevertReason.Erc20InsufficientBalance, + ); + }); + + it('should throw if spender has insufficient allowance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance; + + const spenderAllowance = await token.allowance.callAsync(owner, spender); + const isSpenderAllowanceInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; + expect(isSpenderAllowanceInsufficient).to.be.true(); + + return expectContractCallFailed( + token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }), + RevertReason.Erc20InsufficientAllowance, + ); + }); + + it('should return true on a 0 value transfer', async () => { + const amountToTransfer = new BigNumber(0); + const didReturnTrue = await token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.true(); + }); + + it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await token.allowance.callAsync(owner, spender); + expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); + }); + + it('should transfer the correct balances if spender has sufficient allowance', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newOwnerBalance = await token.balanceOf.callAsync(owner); + const newSpenderBalance = await token.balanceOf.callAsync(spender); + + expect(newOwnerBalance).to.be.bignumber.equal(0); + expect(newSpenderBalance).to.be.bignumber.equal(initOwnerBalance); + }); + + it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await token.allowance.callAsync(owner, spender); + expect(newSpenderAllowance).to.be.bignumber.equal(0); + }); + }); +}); diff --git a/packages/contracts/test/tokens/zrx_token.ts b/packages/contracts/test/tokens/zrx_token.ts new file mode 100644 index 000000000..a0d77c924 --- /dev/null +++ b/packages/contracts/test/tokens/zrx_token.ts @@ -0,0 +1,206 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; + +import { ZRXTokenContract } from '../../generated_contract_wrappers/zrx_token'; +import { artifacts } from '../utils/artifacts'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ZRXToken', () => { + let owner: string; + let spender: string; + let MAX_UINT: BigNumber; + let zrxToken: ZRXTokenContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = accounts[0]; + spender = accounts[1]; + zrxToken = await ZRXTokenContract.deployFrom0xArtifactAsync(artifacts.ZRX, provider, txDefaults); + MAX_UINT = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('constants', () => { + it('should have 18 decimals', async () => { + const decimals = new BigNumber(await zrxToken.decimals.callAsync()); + const expectedDecimals = 18; + expect(decimals).to.be.bignumber.equal(expectedDecimals); + }); + + it('should have a total supply of 1 billion tokens', async () => { + const totalSupply = new BigNumber(await zrxToken.totalSupply.callAsync()); + const expectedTotalSupply = 1000000000; + expect(Web3Wrapper.toUnitAmount(totalSupply, 18)).to.be.bignumber.equal(expectedTotalSupply); + }); + + it('should be named 0x Protocol Token', async () => { + const name = await zrxToken.name.callAsync(); + const expectedName = '0x Protocol Token'; + expect(name).to.be.equal(expectedName); + }); + + it('should have the symbol ZRX', async () => { + const symbol = await zrxToken.symbol.callAsync(); + const expectedSymbol = 'ZRX'; + expect(symbol).to.be.equal(expectedSymbol); + }); + }); + + describe('constructor', () => { + it('should initialize owner balance to totalSupply', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const totalSupply = new BigNumber(await zrxToken.totalSupply.callAsync()); + expect(totalSupply).to.be.bignumber.equal(ownerBalance); + }); + }); + + describe('transfer', () => { + it('should transfer balance from sender to receiver', async () => { + const receiver = spender; + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(receiver, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const finalOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const finalReceiverBalance = await zrxToken.balanceOf.callAsync(receiver); + + const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); + const expectedFinalReceiverBalance = amountToTransfer; + expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); + expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); + }); + + it('should return true on a 0 value transfer', async () => { + const didReturnTrue = await zrxToken.transfer.callAsync(spender, new BigNumber(0), { + from: owner, + }); + expect(didReturnTrue).to.be.true(); + }); + }); + + describe('transferFrom', () => { + it('should return false if owner has insufficient balance', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, amountToTransfer, { + from: owner, + gas: constants.MAX_TOKEN_APPROVE_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.false(); + }); + + it('should return false if spender has insufficient allowance', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance; + + const spenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + const isSpenderAllowanceInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; + expect(isSpenderAllowanceInsufficient).to.be.true(); + + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.false(); + }); + + it('should return true on a 0 value transfer', async () => { + const amountToTransfer = new BigNumber(0); + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.true(); + }); + + it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = MAX_UINT; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, initSpenderAllowance, { + from: owner, + gas: constants.MAX_TOKEN_APPROVE_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); + }); + + it('should transfer the correct balances if spender has sufficient allowance', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const initSpenderBalance = await zrxToken.balanceOf.callAsync(spender); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, initSpenderAllowance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const newSpenderBalance = await zrxToken.balanceOf.callAsync(spender); + + expect(newOwnerBalance).to.be.bignumber.equal(0); + expect(newSpenderBalance).to.be.bignumber.equal(initSpenderBalance.plus(initOwnerBalance)); + }); + + it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, amountToTransfer), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + expect(newSpenderAllowance).to.be.bignumber.equal(0); + }); + }); +}); diff --git a/packages/contracts/test/tutorials/arbitrage.ts b/packages/contracts/test/tutorials/arbitrage.ts index b2bbaf420..6851483cc 100644 --- a/packages/contracts/test/tutorials/arbitrage.ts +++ b/packages/contracts/test/tutorials/arbitrage.ts @@ -1,225 +1,260 @@ -import { ECSignature, SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import ethUtil = require('ethereumjs-util'); -import * as Web3 from 'web3'; +// import { ECSignature, SignedOrder, ZeroEx } from '0x.js'; +// import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +// import { ExchangeContractErrs } from 'ethereum-types'; +// import { BigNumber } from '@0xproject/utils'; +// import { Web3Wrapper } from '@0xproject/web3-wrapper'; +// import * as chai from 'chai'; +// import ethUtil = require('ethereumjs-util'); +// import * as Web3 from 'web3'; -import { ArbitrageContract } from '../../src/contract_wrappers/generated/arbitrage'; -import { EtherDeltaContract } from '../../src/contract_wrappers/generated/ether_delta'; -import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { crypto } from '../../util/crypto'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +// import { AccountLevelsContract } from '../../src/generated_contract_wrappers/account_levels'; +// import { ArbitrageContract } from '../../src/generated_contract_wrappers/arbitrage'; +// import { DummyTokenContract } from '../../src/generated_contract_wrappers/dummy_token'; +// import { EtherDeltaContract } from '../../src/generated_contract_wrappers/ether_delta'; +// import { ExchangeContract } from '../../src/generated_contract_wrappers/exchange'; +// import { TokenTransferProxyContract } from '../../src/generated_contract_wrappers/token_transfer_proxy'; +// import { artifacts } from '../../util/artifacts'; +// import { Balances } from '../../util/balances'; +// import { constants } from '../../util/constants'; +// import { crypto } from '../../util/crypto'; +// import { ExchangeWrapper } from '../../util/exchange_wrapper'; +// import { OrderFactory } from '../../util/order_factory'; +// import { BalancesByOwner, ContractName } from '../../util/types'; +// import { chaiSetup } from '../utils/chai_setup'; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; -describe('Arbitrage', () => { - let coinbase: string; - let maker: string; - let edMaker: string; - let edFrontRunner: string; - let amountGet: BigNumber; - let amountGive: BigNumber; - let makerTokenAmount: BigNumber; - let takerTokenAmount: BigNumber; - const feeRecipient = ZeroEx.NULL_ADDRESS; - const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +// chaiSetup.configure(); +// const expect = chai.expect; +// const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - let weth: Web3.ContractInstance; - let zrx: Web3.ContractInstance; - let arbitrage: ArbitrageContract; - let etherDelta: EtherDeltaContract; +// describe('Arbitrage', () => { +// let coinbase: string; +// let maker: string; +// let edMaker: string; +// let edFrontRunner: string; +// let amountGet: BigNumber; +// let amountGive: BigNumber; +// let makerTokenAmount: BigNumber; +// let takerTokenAmount: BigNumber; +// const feeRecipient = constants.NULL_ADDRESS; +// const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +// const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - let signedOrder: SignedOrder; - let exWrapper: ExchangeWrapper; - let orderFactory: OrderFactory; +// let weth: DummyTokenContract; +// let zrx: DummyTokenContract; +// let arbitrage: ArbitrageContract; +// let etherDelta: EtherDeltaContract; - let zeroEx: ZeroEx; +// let signedOrder: SignedOrder; +// let exWrapper: ExchangeWrapper; +// let orderFactory: OrderFactory; - // From a bird's eye view - we create two orders. - // 0x order of 1 ZRX (maker) for 1 WETH (taker) - // ED order of 2 WETH (tokenGive) for 1 ZRX (tokenGet) - // And then we do an atomic arbitrage between them which gives us 1 WETH. - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - [coinbase, maker, edMaker, edFrontRunner] = accounts; - weth = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - zrx = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - const accountLevels = await deployer.deployAsync(ContractName.AccountLevels); - const edAdminAddress = accounts[0]; - const edMakerFee = 0; - const edTakerFee = 0; - const edFeeRebate = 0; - const etherDeltaInstance = await deployer.deployAsync(ContractName.EtherDelta, [ - edAdminAddress, - feeRecipient, - accountLevels.address, - edMakerFee, - edTakerFee, - edFeeRebate, - ]); - etherDelta = new EtherDeltaContract(web3Wrapper, etherDeltaInstance.abi, etherDeltaInstance.address); - const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - await tokenTransferProxy.addAuthorizedAddress(exchangeInstance.address, { from: accounts[0] }); - zeroEx = new ZeroEx(web3.currentProvider, { - exchangeContractAddress: exchangeInstance.address, - networkId: constants.TESTRPC_NETWORK_ID, - }); - const exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - exWrapper = new ExchangeWrapper(exchange, zeroEx); +// let zeroEx: ZeroEx; - makerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); - takerTokenAmount = makerTokenAmount; - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: zrx.address, - takerTokenAddress: weth.address, - makerTokenAmount, - takerTokenAmount, - makerFee: new BigNumber(0), - takerFee: new BigNumber(0), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - const arbitrageInstance = await deployer.deployAsync(ContractName.Arbitrage, [ - exchange.address, - etherDelta.address, - tokenTransferProxy.address, - ]); - arbitrage = new ArbitrageContract(web3Wrapper, arbitrageInstance.abi, arbitrageInstance.address); - // Enable arbitrage and withdrawals of tokens - await arbitrage.setAllowances.sendTransactionAsync(weth.address, { from: coinbase }); - await arbitrage.setAllowances.sendTransactionAsync(zrx.address, { from: coinbase }); +// // From a bird's eye view - we create two orders. +// // 0x order of 1 ZRX (maker) for 1 WETH (taker) +// // ED order of 2 WETH (tokenGive) for 1 ZRX (tokenGet) +// // And then we do an atomic arbitrage between them which gives us 1 WETH. +// before(async () => { +// const accounts = await web3Wrapper.getAvailableAddressesAsync(); +// [coinbase, maker, edMaker, edFrontRunner] = accounts; +// weth = await DummyTokenContract.deployFrom0xArtifactAsync( +// artifacts.DummyToken, +// provider, +// txDefaults, +// constants.DUMMY_TOKEN_NAME, +// constants.DUMMY_TOKEN_SYMBOL, +// constants.DUMMY_TOKEN_DECIMALS, +// constants.DUMMY_TOKEN_TOTAL_SUPPLY, +// ); +// zrx = await DummyTokenContract.deployFrom0xArtifactAsync( +// artifacts.DummyToken, +// provider, +// txDefaults, +// constants.DUMMY_TOKEN_NAME, +// constants.DUMMY_TOKEN_SYMBOL, +// constants.DUMMY_TOKEN_DECIMALS, +// constants.DUMMY_TOKEN_TOTAL_SUPPLY, +// ); +// const accountLevels = await AccountLevelsContract.deployFrom0xArtifactAsync( +// artifacts.AccountLevels, +// provider, +// txDefaults, +// ); +// const edAdminAddress = accounts[0]; +// const edMakerFee = new BigNumber(0); +// const edTakerFee = new BigNumber(0); +// const edFeeRebate = new BigNumber(0); +// etherDelta = await EtherDeltaContract.deployFrom0xArtifactAsync( +// artifacts.EtherDelta, +// provider, +// txDefaults, +// edAdminAddress, +// feeRecipient, +// accountLevels.address, +// edMakerFee, +// edTakerFee, +// edFeeRebate, +// ); +// const tokenTransferProxy = await TokenTransferProxyContract.deployFrom0xArtifactAsync( +// artifacts.TokenTransferProxy, +// provider, +// txDefaults, +// ); +// const exchange = await ExchangeContract.deployFrom0xArtifactAsync( +// artifacts.Exchange, +// provider, +// txDefaults, +// zrx.address, +// tokenTransferProxy.address, +// ); +// await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); +// zeroEx = new ZeroEx(provider, { +// exchangeContractAddress: exchange.address, +// networkId: constants.TESTRPC_NETWORK_ID, +// }); +// exWrapper = new ExchangeWrapper(exchange, provider); - // Give some tokens to arbitrage contract - await weth.setBalance(arbitrage.address, takerTokenAmount, { from: coinbase }); +// makerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); +// takerTokenAmount = makerTokenAmount; +// const defaultOrderParams = { +// exchangeContractAddress: exchange.address, +// maker, +// feeRecipient, +// makerTokenAddress: zrx.address, +// takerTokenAddress: weth.address, +// makerTokenAmount, +// takerTokenAmount, +// makerFee: new BigNumber(0), +// takerFee: new BigNumber(0), +// }; +// orderFactory = new OrderFactory(zeroEx, defaultOrderParams); +// arbitrage = await ArbitrageContract.deployFrom0xArtifactAsync( +// artifacts.Arbitrage, +// provider, +// txDefaults, +// exchange.address, +// etherDelta.address, +// tokenTransferProxy.address, +// ); +// // Enable arbitrage and withdrawals of tokens +// await arbitrage.setAllowances.sendTransactionAsync(weth.address, { from: coinbase }); +// await arbitrage.setAllowances.sendTransactionAsync(zrx.address, { from: coinbase }); - // Fund the maker on exchange side - await zrx.setBalance(maker, makerTokenAmount, { from: coinbase }); - // Set the allowance for the maker on Exchange side - await zrx.approve(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); +// // Give some tokens to arbitrage contract +// await weth.setBalance.sendTransactionAsync(arbitrage.address, takerTokenAmount, { from: coinbase }); - amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); - // Fund the maker on EtherDelta side - await weth.setBalance(edMaker, amountGive, { from: coinbase }); - // Set the allowance for the maker on EtherDelta side - await weth.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker }); - // Deposit maker funds into EtherDelta - await etherDelta.depositToken.sendTransactionAsync(weth.address, amountGive, { from: edMaker }); +// // Fund the maker on exchange side +// await zrx.setBalance.sendTransactionAsync(maker, makerTokenAmount, { from: coinbase }); +// // Set the allowance for the maker on Exchange side +// await zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); - amountGet = makerTokenAmount; - // Fund the front runner on EtherDelta side - await zrx.setBalance(edFrontRunner, amountGet, { from: coinbase }); - // Set the allowance for the front-runner on EtherDelta side - await zrx.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner }); - // Deposit front runner funds into EtherDelta - await etherDelta.depositToken.sendTransactionAsync(zrx.address, amountGet, { from: edFrontRunner }); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('makeAtomicTrade', () => { - let addresses: string[]; - let values: BigNumber[]; - let v: number[]; - let r: string[]; - let s: string[]; - let tokenGet: string; - let tokenGive: string; - let expires: BigNumber; - let nonce: BigNumber; - let edSignature: ECSignature; - before(async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - tokenGet = zrx.address; - tokenGive = weth.address; - const blockNumber = await web3Wrapper.getBlockNumberAsync(); - const ED_ORDER_EXPIRATION_IN_BLOCKS = 10; - expires = new BigNumber(blockNumber + ED_ORDER_EXPIRATION_IN_BLOCKS); - nonce = new BigNumber(42); - const edOrderHash = `0x${crypto - .solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce]) - .toString('hex')}`; - const shouldAddPersonalMessagePrefix = false; - edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix); - addresses = [ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - edMaker, - ]; - const fillTakerTokenAmount = takerTokenAmount; - const edFillAmount = makerTokenAmount; - values = [ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - fillTakerTokenAmount, - amountGet, - amountGive, - expires, - nonce, - edFillAmount, - ]; - v = [signedOrder.ecSignature.v, edSignature.v]; - r = [signedOrder.ecSignature.r, edSignature.r]; - s = [signedOrder.ecSignature.s, edSignature.s]; - }); - it('should successfully execute the arbitrage if not front-runned', async () => { - const txHash = await arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { - from: coinbase, - }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const postBalance = await weth.balanceOf(arbitrage.address); - expect(postBalance).to.be.bignumber.equal(amountGive); - }); - it('should fail and revert if front-runned', async () => { - const preBalance = await weth.balanceOf(arbitrage.address); - // Front-running transaction - await etherDelta.trade.sendTransactionAsync( - tokenGet, - amountGet, - tokenGive, - amountGive, - expires, - nonce, - edMaker, - edSignature.v, - edSignature.r, - edSignature.s, - amountGet, - { from: edFrontRunner }, - ); - // tslint:disable-next-line:await-promise - await expect( - arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { from: coinbase }), - ).to.be.rejectedWith(constants.REVERT); - const postBalance = await weth.balanceOf(arbitrage.address); - expect(preBalance).to.be.bignumber.equal(postBalance); - }); - }); -}); +// amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); +// // Fund the maker on EtherDelta side +// await weth.setBalance.sendTransactionAsync(edMaker, amountGive, { from: coinbase }); +// // Set the allowance for the maker on EtherDelta side +// await weth.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker }); +// // Deposit maker funds into EtherDelta +// await etherDelta.depositToken.sendTransactionAsync(weth.address, amountGive, { from: edMaker }); + +// amountGet = makerTokenAmount; +// // Fund the front runner on EtherDelta side +// await zrx.setBalance.sendTransactionAsync(edFrontRunner, amountGet, { from: coinbase }); +// // Set the allowance for the front-runner on EtherDelta side +// await zrx.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner }); +// // Deposit front runner funds into EtherDelta +// await etherDelta.depositToken.sendTransactionAsync(zrx.address, amountGet, { from: edFrontRunner }); +// }); +// beforeEach(async () => { +// await blockchainLifecycle.startAsync(); +// }); +// afterEach(async () => { +// await blockchainLifecycle.revertAsync(); +// }); +// describe('makeAtomicTrade', () => { +// let addresses: string[]; +// let values: BigNumber[]; +// let v: number[]; +// let r: string[]; +// let s: string[]; +// let tokenGet: string; +// let tokenGive: string; +// let expires: BigNumber; +// let nonce: BigNumber; +// let edSignature: ECSignature; +// before(async () => { +// signedOrder = await orderFactory.newSignedOrderAsync(); +// tokenGet = zrx.address; +// tokenGive = weth.address; +// const blockNumber = await web3Wrapper.getBlockNumberAsync(); +// const ED_ORDER_EXPIRATION_IN_BLOCKS = 10; +// expires = new BigNumber(blockNumber + ED_ORDER_EXPIRATION_IN_BLOCKS); +// nonce = new BigNumber(42); +// const edOrderHash = `0x${crypto +// .solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce]) +// .toString('hex')}`; +// const shouldAddPersonalMessagePrefix = false; +// edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix); +// addresses = [ +// signedOrder.maker, +// signedOrder.taker, +// signedOrder.makerTokenAddress, +// signedOrder.takerTokenAddress, +// signedOrder.feeRecipient, +// edMaker, +// ]; +// const fillTakerTokenAmount = takerTokenAmount; +// const edFillAmount = makerTokenAmount; +// values = [ +// signedOrder.makerTokenAmount, +// signedOrder.takerTokenAmount, +// signedOrder.makerFee, +// signedOrder.takerFee, +// signedOrder.expirationUnixTimestampSec, +// signedOrder.salt, +// fillTakerTokenAmount, +// amountGet, +// amountGive, +// expires, +// nonce, +// edFillAmount, +// ]; +// v = [signedOrder.ecSignature.v, edSignature.v]; +// r = [signedOrder.ecSignature.r, edSignature.r]; +// s = [signedOrder.ecSignature.s, edSignature.s]; +// }); +// it('should successfully execute the arbitrage if not front-runned', async () => { +// const txHash = await arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { +// from: coinbase, +// }); +// const res = await zeroEx.awaitTransactionMinedAsync(txHash); +// const postBalance = await weth.balanceOf.callAsync(arbitrage.address); +// expect(postBalance).to.be.bignumber.equal(amountGive); +// }); +// it('should fail and revert if front-runned', async () => { +// const preBalance = await weth.balanceOf.callAsync(arbitrage.address); +// // Front-running transaction +// await etherDelta.trade.sendTransactionAsync( +// tokenGet, +// amountGet, +// tokenGive, +// amountGive, +// expires, +// nonce, +// edMaker, +// edSignature.v, +// edSignature.r, +// edSignature.s, +// amountGet, +// { from: edFrontRunner }, +// ); +// // tslint:disable-next-line:await-promise +// await expect( +// arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { from: coinbase }), +// ).to.be.rejectedWith(constants.REVERT); +// const postBalance = await weth.balanceOf.callAsync(arbitrage.address); +// expect(preBalance).to.be.bignumber.equal(postBalance); +// }); +// }); +// }); diff --git a/packages/contracts/test/unlimited_allowance_token.ts b/packages/contracts/test/unlimited_allowance_token.ts deleted file mode 100644 index c1c518adc..000000000 --- a/packages/contracts/test/unlimited_allowance_token.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../src/contract_wrappers/generated/dummy_token'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('UnlimitedAllowanceToken', () => { - let owner: string; - let spender: string; - const config = { - networkId: constants.TESTRPC_NETWORK_ID, - }; - const zeroEx = new ZeroEx(web3.currentProvider, config); - - const MAX_MINT_VALUE = new BigNumber(100000000000000000000); - let tokenAddress: string; - let token: DummyTokenContract; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = accounts[0]; - spender = accounts[1]; - const tokenInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - token = new DummyTokenContract(web3Wrapper, tokenInstance.abi, tokenInstance.address); - await token.mint.sendTransactionAsync(MAX_MINT_VALUE, { from: owner }); - tokenAddress = token.address; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('transfer', () => { - it('should throw if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - return expect(token.transfer.callAsync(spender, amountToTransfer, { from: owner })).to.be.rejectedWith( - constants.REVERT, - ); - }); - - it('should transfer balance from sender to receiver', async () => { - const receiver = spender; - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = new BigNumber(1); - await zeroEx.token.transferAsync(tokenAddress, owner, receiver, amountToTransfer); - const finalOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const finalReceiverBalance = await zeroEx.token.getBalanceAsync(tokenAddress, receiver); - - const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); - const expectedFinalReceiverBalance = amountToTransfer; - expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); - expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); - }); - - it('should return true on a 0 value transfer', async () => { - const didReturnTrue = await token.transfer.callAsync(spender, new BigNumber(0), { - from: owner, - }); - expect(didReturnTrue).to.be.true(); - }); - }); - - describe('transferFrom', () => { - it('should throw if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, amountToTransfer); - return expect( - token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if spender has insufficient allowance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance; - - const spenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - const spenderAllowanceIsInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; - expect(spenderAllowanceIsInsufficient).to.be.true(); - - return expect( - token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should return true on a 0 value transfer', async () => { - const amountToTransfer = new BigNumber(0); - const didReturnTrue = await token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }); - expect(didReturnTrue).to.be.true(); - }); - - it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); - }); - - it('should transfer the correct balances if spender has sufficient allowance', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const newSpenderBalance = await zeroEx.token.getBalanceAsync(tokenAddress, spender); - - expect(newOwnerBalance).to.be.bignumber.equal(0); - expect(newSpenderBalance).to.be.bignumber.equal(initOwnerBalance); - }); - - it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - expect(newSpenderAllowance).to.be.bignumber.equal(0); - }); - }); -}); diff --git a/packages/contracts/test/utils/abstract_asset_wrapper.ts b/packages/contracts/test/utils/abstract_asset_wrapper.ts new file mode 100644 index 000000000..4b56a8502 --- /dev/null +++ b/packages/contracts/test/utils/abstract_asset_wrapper.ts @@ -0,0 +1,3 @@ +export abstract class AbstractAssetWrapper { + public abstract getProxyId(): string; +} diff --git a/packages/contracts/test/utils/address_utils.ts b/packages/contracts/test/utils/address_utils.ts new file mode 100644 index 000000000..a9fb6921a --- /dev/null +++ b/packages/contracts/test/utils/address_utils.ts @@ -0,0 +1,10 @@ +import { crypto, generatePseudoRandomSalt } from '@0xproject/order-utils'; + +export const addressUtils = { + generatePseudoRandomAddress(): string { + const randomBigNum = generatePseudoRandomSalt(); + const randomBuff = crypto.solSHA3([randomBigNum]); + const randomAddress = `0x${randomBuff.slice(0, 20).toString('hex')}`; + return randomAddress; + }, +}; diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts new file mode 100644 index 000000000..e608ee174 --- /dev/null +++ b/packages/contracts/test/utils/artifacts.ts @@ -0,0 +1,57 @@ +import { ContractArtifact } from '@0xproject/sol-compiler'; + +import * as AssetProxyOwner from '../../artifacts/AssetProxyOwner.json'; +import * as DummyERC20Token from '../../artifacts/DummyERC20Token.json'; +import * as DummyERC721Receiver from '../../artifacts/DummyERC721Receiver.json'; +import * as DummyERC721Token from '../../artifacts/DummyERC721Token.json'; +import * as ERC20Proxy from '../../artifacts/ERC20Proxy.json'; +import * as ERC721Proxy from '../../artifacts/ERC721Proxy.json'; +import * as Exchange from '../../artifacts/Exchange.json'; +import * as ExchangeWrapper from '../../artifacts/ExchangeWrapper.json'; +import * as Forwarder from '../../artifacts/Forwarder.json'; +import * as IAssetProxy from '../../artifacts/IAssetProxy.json'; +import * as MixinAuthorizable from '../../artifacts/MixinAuthorizable.json'; +import * as MultiSigWallet from '../../artifacts/MultiSigWallet.json'; +import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithTimeLock.json'; +import * as TestAssetProxyDispatcher from '../../artifacts/TestAssetProxyDispatcher.json'; +import * as TestAssetProxyOwner from '../../artifacts/TestAssetProxyOwner.json'; +import * as TestConstants from '../../artifacts/TestConstants.json'; +import * as TestExchangeInternals from '../../artifacts/TestExchangeInternals.json'; +import * as TestLibBytes from '../../artifacts/TestLibBytes.json'; +import * as TestLibs from '../../artifacts/TestLibs.json'; +import * as TestSignatureValidator from '../../artifacts/TestSignatureValidator.json'; +import * as TokenRegistry from '../../artifacts/TokenRegistry.json'; +import * as Validator from '../../artifacts/Validator.json'; +import * as Wallet from '../../artifacts/Wallet.json'; +import * as EtherToken from '../../artifacts/WETH9.json'; +import * as Whitelist from '../../artifacts/Whitelist.json'; +import * as ZRX from '../../artifacts/ZRXToken.json'; + +export const artifacts = { + AssetProxyOwner: (AssetProxyOwner as any) as ContractArtifact, + DummyERC20Token: (DummyERC20Token as any) as ContractArtifact, + DummyERC721Receiver: (DummyERC721Receiver as any) as ContractArtifact, + DummyERC721Token: (DummyERC721Token as any) as ContractArtifact, + ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, + ERC721Proxy: (ERC721Proxy as any) as ContractArtifact, + Exchange: (Exchange as any) as ContractArtifact, + ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact, + EtherToken: (EtherToken as any) as ContractArtifact, + Forwarder: (Forwarder as any) as ContractArtifact, + IAssetProxy: (IAssetProxy as any) as ContractArtifact, + MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact, + MultiSigWallet: (MultiSigWallet as any) as ContractArtifact, + MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact, + TestAssetProxyOwner: (TestAssetProxyOwner as any) as ContractArtifact, + TestAssetProxyDispatcher: (TestAssetProxyDispatcher as any) as ContractArtifact, + TestConstants: (TestConstants as any) as ContractArtifact, + TestLibBytes: (TestLibBytes as any) as ContractArtifact, + TestLibs: (TestLibs as any) as ContractArtifact, + TestExchangeInternals: (TestExchangeInternals as any) as ContractArtifact, + TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact, + Validator: (Validator as any) as ContractArtifact, + Wallet: (Wallet as any) as ContractArtifact, + TokenRegistry: (TokenRegistry as any) as ContractArtifact, + Whitelist: (Whitelist as any) as ContractArtifact, + ZRX: (ZRX as any) as ContractArtifact, +}; diff --git a/packages/contracts/test/utils/assertions.ts b/packages/contracts/test/utils/assertions.ts new file mode 100644 index 000000000..61df800c8 --- /dev/null +++ b/packages/contracts/test/utils/assertions.ts @@ -0,0 +1,186 @@ +import { RevertReason } from '@0xproject/types'; +import { logUtils } from '@0xproject/utils'; +import { NodeType } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceipt, TransactionReceiptStatus, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { web3Wrapper } from './web3_wrapper'; + +const expect = chai.expect; + +let nodeType: NodeType | undefined; + +// Represents the return value of a `sendTransaction` call. The Promise should +// resolve with either a transaction receipt or a transaction hash. +export type sendTransactionResult = Promise<TransactionReceipt | TransactionReceiptWithDecodedLogs | string>; + +/** + * Returns ganacheError if the backing Ethereum node is Ganache and gethError + * if it is Geth. + * @param ganacheError the error to be returned if the backing node is Ganache. + * @param gethError the error to be returned if the backing node is Geth. + * @returns either the given ganacheError or gethError depending on the backing + * node. + */ +async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise<string> { + if (_.isUndefined(nodeType)) { + nodeType = await web3Wrapper.getNodeTypeAsync(); + } + switch (nodeType) { + case NodeType.Ganache: + return ganacheError; + case NodeType.Geth: + return gethError; + default: + throw new Error(`Unknown node type: ${nodeType}`); + } +} + +async function _getInsufficientFundsErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError("sender doesn't have enough funds", 'insufficient funds'); +} + +async function _getTransactionFailedErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError('revert', 'always failing transaction'); +} + +async function _getContractCallFailedErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError('revert', 'Contract call failed'); +} + +/** + * Returns the expected error message for an 'invalid opcode' resulting from a + * contract call. The exact error message depends on the backing Ethereum node. + */ +export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise<string> { + return _getGanacheOrGethError('invalid opcode', 'Contract call failed'); +} + +/** + * Returns the expected error message for the given revert reason resulting from + * a sendTransaction call. The exact error message depends on the backing + * Ethereum node and whether it supports revert reasons. + * @param reason a specific revert reason. + * @returns the expected error message. + */ +export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise<string> { + return _getGanacheOrGethError(reason, 'always failing transaction'); +} + +/** + * Rejects if the given Promise does not reject with an error indicating + * insufficient funds. + * @param p a promise resulting from a contract call or sendTransaction call. + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectInsufficientFundsAsync<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getInsufficientFundsErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} + +/** + * Resolves if the the sendTransaction call fails with the given revert reason. + * However, since Geth does not support revert reasons for sendTransaction, this + * falls back to expectTransactionFailedWithoutReasonAsync if the backing + * Ethereum node is Geth. + * @param p a Promise resulting from a sendTransaction call + * @param reason a specific revert reason + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectTransactionFailedAsync(p: sendTransactionResult, reason: RevertReason): Promise<void> { + // HACK(albrow): This dummy `catch` should not be necessary, but if you + // remove it, there is an uncaught exception and the Node process will + // forcibly exit. It's possible this is a false positive in + // make-promises-safe. + p.catch(e => { + _.noop(e); + }); + + if (_.isUndefined(nodeType)) { + nodeType = await web3Wrapper.getNodeTypeAsync(); + } + switch (nodeType) { + case NodeType.Ganache: + return expect(p).to.be.rejectedWith(reason); + case NodeType.Geth: + logUtils.warn( + 'WARNING: Geth does not support revert reasons for sendTransaction. This test will pass if the transaction fails for any reason.', + ); + return expectTransactionFailedWithoutReasonAsync(p); + default: + throw new Error(`Unknown node type: ${nodeType}`); + } +} + +/** + * Resolves if the transaction fails without a revert reason, or if the + * corresponding transactionReceipt has a status of 0 or '0', indicating + * failure. + * @param p a Promise resulting from a sendTransaction call + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectTransactionFailedWithoutReasonAsync(p: sendTransactionResult): Promise<void> { + return p + .then(async result => { + let txReceiptStatus: TransactionReceiptStatus; + if (_.isString(result)) { + // Result is a txHash. We need to make a web3 call to get the + // receipt, then get the status from the receipt. + const txReceipt = await web3Wrapper.awaitTransactionMinedAsync(result); + txReceiptStatus = txReceipt.status; + } else if ('status' in result) { + // Result is a transaction receipt, so we can get the status + // directly. + txReceiptStatus = result.status; + } else { + throw new Error('Unexpected result type: ' + typeof result); + } + expect(_.toString(txReceiptStatus)).to.equal( + '0', + 'Expected transaction to fail but receipt had a non-zero status, indicating success', + ); + }) + .catch(async err => { + // If the promise rejects, we expect a specific error message, + // depending on the backing Ethereum node type. + const errMessage = await _getTransactionFailedErrorMessageAsync(); + expect(err.message).to.include(errMessage); + }); +} + +/** + * Resolves if the the contract call fails with the given revert reason. + * @param p a Promise resulting from a contract call + * @param reason a specific revert reason + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCallFailed<T>(p: Promise<T>, reason: RevertReason): Promise<void> { + return expect(p).to.be.rejectedWith(reason); +} + +/** + * Resolves if the contract call fails without a revert reason. + * @param p a Promise resulting from a contract call + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCallFailedWithoutReasonAsync<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getContractCallFailedErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} + +/** + * Resolves if the contract creation/deployment fails without a revert reason. + * @param p a Promise resulting from a contract creation/deployment + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCreationFailedWithoutReason<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getTransactionFailedErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} diff --git a/packages/contracts/test/utils/asset_wrapper.ts b/packages/contracts/test/utils/asset_wrapper.ts new file mode 100644 index 000000000..e3a4800c6 --- /dev/null +++ b/packages/contracts/test/utils/asset_wrapper.ts @@ -0,0 +1,223 @@ +import { assetDataUtils } from '@0xproject/order-utils'; +import { AssetProxyId } from '@0xproject/types'; +import { BigNumber, errorUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { AbstractAssetWrapper } from './abstract_asset_wrapper'; +import { constants } from './constants'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; + +interface ProxyIdToAssetWrappers { + [proxyId: string]: AbstractAssetWrapper; +} + +/** + * This class abstracts away the differences between ERC20 and ERC721 tokens so that + * the logic that uses it does not need to care what standard a token belongs to. + */ +export class AssetWrapper { + private readonly _proxyIdToAssetWrappers: ProxyIdToAssetWrappers; + constructor(assetWrappers: AbstractAssetWrapper[]) { + this._proxyIdToAssetWrappers = {}; + _.each(assetWrappers, assetWrapper => { + const proxyId = assetWrapper.getProxyId(); + this._proxyIdToAssetWrappers[proxyId] = assetWrapper; + }); + } + public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + const balance = await erc20Wrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + case AssetProxyId.ERC721: { + const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const isOwner = await assetWrapper.isOwnerAsync( + userAddress, + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + const balance = isOwner ? new BigNumber(1) : new BigNumber(0); + return balance; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async setBalanceAsync(userAddress: string, assetData: string, desiredBalance: BigNumber): Promise<void> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + await erc20Wrapper.setBalanceAsync(userAddress, assetData, desiredBalance); + return; + } + case AssetProxyId.ERC721: { + if (!desiredBalance.eq(0) && !desiredBalance.eq(1)) { + throw new Error(`Balance for ERC721 token can only be set to 0 or 1. Got: ${desiredBalance}`); + } + const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const doesTokenExist = erc721Wrapper.doesTokenExistAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!doesTokenExist && desiredBalance.eq(1)) { + await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress); + return; + } else if (!doesTokenExist && desiredBalance.eq(0)) { + return; // noop + } + const tokenOwner = await erc721Wrapper.ownerOfAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (userAddress !== tokenOwner && desiredBalance.eq(1)) { + await erc721Wrapper.transferFromAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + tokenOwner, + userAddress, + ); + } else if (tokenOwner === userAddress && desiredBalance.eq(0)) { + // Transfer token to someone else + const userAddresses = await (erc721Wrapper as any)._web3Wrapper.getAvailableAddressesAsync(); + const nonOwner = _.find(userAddresses, a => a !== userAddress); + await erc721Wrapper.transferFromAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + tokenOwner, + nonOwner, + ); + return; + } else if ( + (userAddress !== tokenOwner && desiredBalance.eq(0)) || + (tokenOwner === userAddress && desiredBalance.eq(1)) + ) { + return; // noop + } + break; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + const allowance = await erc20Wrapper.getProxyAllowanceAsync(userAddress, assetData); + return allowance; + } + case AssetProxyId.ERC721: { + const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const erc721ProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const isProxyApprovedForAll = await assetWrapper.isProxyApprovedForAllAsync( + userAddress, + erc721ProxyData.tokenAddress, + ); + if (isProxyApprovedForAll) { + return constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + } + + const isProxyApproved = await assetWrapper.isProxyApprovedAsync( + erc721ProxyData.tokenAddress, + erc721ProxyData.tokenId, + ); + const allowance = isProxyApproved ? new BigNumber(1) : new BigNumber(0); + return allowance; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async setProxyAllowanceAsync( + userAddress: string, + assetData: string, + desiredAllowance: BigNumber, + ): Promise<void> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + await erc20Wrapper.setAllowanceAsync(userAddress, assetData, desiredAllowance); + return; + } + case AssetProxyId.ERC721: { + if ( + !desiredAllowance.eq(0) && + !desiredAllowance.eq(1) && + !desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS) + ) { + throw new Error( + `Allowance for ERC721 token can only be set to 0, 1 or 2^256-1. Got: ${desiredAllowance}`, + ); + } + const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + + const doesTokenExist = await erc721Wrapper.doesTokenExistAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!doesTokenExist) { + throw new Error( + `Cannot setProxyAllowance on non-existent token: ${assetProxyData.tokenAddress} ${ + assetProxyData.tokenId + }`, + ); + } + const isProxyApprovedForAll = await erc721Wrapper.isProxyApprovedForAllAsync( + userAddress, + assetProxyData.tokenAddress, + ); + if (!isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + const isApproved = true; + await erc721Wrapper.approveProxyForAllAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + isApproved, + ); + } else if (isProxyApprovedForAll && desiredAllowance.eq(0)) { + const isApproved = false; + await erc721Wrapper.approveProxyForAllAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + isApproved, + ); + } else if (isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + return; // Noop + } + + const isProxyApproved = await erc721Wrapper.isProxyApprovedAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!isProxyApproved && desiredAllowance.eq(1)) { + await erc721Wrapper.approveProxyAsync(assetProxyData.tokenAddress, assetProxyData.tokenId); + } else if (isProxyApproved && desiredAllowance.eq(0)) { + // Remove approval + await erc721Wrapper.approveAsync( + constants.NULL_ADDRESS, + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + } else if ( + (!isProxyApproved && desiredAllowance.eq(0)) || + (isProxyApproved && desiredAllowance.eq(1)) + ) { + return; // noop + } + + break; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } +} diff --git a/packages/contracts/test/utils/block_timestamp.ts b/packages/contracts/test/utils/block_timestamp.ts new file mode 100644 index 000000000..1159792c4 --- /dev/null +++ b/packages/contracts/test/utils/block_timestamp.ts @@ -0,0 +1,40 @@ +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { web3Wrapper } from './web3_wrapper'; + +let firstAccount: string | undefined; + +/** + * Increases time by the given number of seconds and then mines a block so that + * the current block timestamp has the offset applied. + * @param seconds the number of seconds by which to incrase the time offset. + * @returns a new Promise which will resolve with the new total time offset or + * reject if the time could not be increased. + */ +export async function increaseTimeAndMineBlockAsync(seconds: number): Promise<number> { + if (_.isUndefined(firstAccount)) { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + firstAccount = accounts[0]; + } + + const offset = await web3Wrapper.increaseTimeAsync(seconds); + // Note: we need to send a transaction after increasing time so + // that a block is actually mined. The contract looks at the + // last mined block for the timestamp. + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ from: firstAccount, to: firstAccount, value: 0 }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + return offset; +} + +/** + * Returns the timestamp of the latest block in seconds since the Unix epoch. + * @returns a new Promise which will resolve with the timestamp in seconds. + */ +export async function getLatestBlockTimestampAsync(): Promise<number> { + const currentBlock = await web3Wrapper.getBlockAsync('latest'); + return currentBlock.timestamp; +} diff --git a/packages/contracts/test/utils/chai_setup.ts b/packages/contracts/test/utils/chai_setup.ts index 078edd309..1a8733093 100644 --- a/packages/contracts/test/utils/chai_setup.ts +++ b/packages/contracts/test/utils/chai_setup.ts @@ -4,7 +4,7 @@ import ChaiBigNumber = require('chai-bignumber'); import * as dirtyChai from 'dirty-chai'; export const chaiSetup = { - configure() { + configure(): void { chai.config.includeStack = true; chai.use(ChaiBigNumber()); chai.use(dirtyChai); diff --git a/packages/contracts/test/utils/combinatorial_utils.ts b/packages/contracts/test/utils/combinatorial_utils.ts new file mode 100644 index 000000000..d72b41f8a --- /dev/null +++ b/packages/contracts/test/utils/combinatorial_utils.ts @@ -0,0 +1,113 @@ +import { BigNumber } from '@0xproject/utils'; +import * as combinatorics from 'js-combinatorics'; + +import { testWithReferenceFuncAsync } from './test_with_reference'; + +// A set of values corresponding to the uint256 type in Solidity. This set +// contains some notable edge cases, including some values which will overflow +// the uint256 type when used in different mathematical operations. +export const uint256Values = [ + new BigNumber(0), + new BigNumber(1), + new BigNumber(2), + // Non-trivial big number. + new BigNumber(2).pow(64), + // Max that does not overflow when squared. + new BigNumber(2).pow(128).minus(1), + // Min that does overflow when squared. + new BigNumber(2).pow(128), + // Max that does not overflow when doubled. + new BigNumber(2).pow(255).minus(1), + // Min that does overflow when doubled. + new BigNumber(2).pow(255), + // Max that does not overflow. + new BigNumber(2).pow(256).minus(1), +]; + +// A set of values corresponding to the bytes32 type in Solidity. +export const bytes32Values = [ + // Min + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + // Non-trivial big number. + '0x000000000000f000000000000000000000000000000000000000000000000000', + // Max + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', +]; + +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, R>( + name: string, + referenceFunc: (p0: P0, p1: P1) => Promise<R>, + testFunc: (p0: P0, p1: P1) => Promise<R>, + allValues: [P0[], P1[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + allValues: [P0[], P1[], P2[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + allValues: [P0[], P1[], P2[], P3[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + allValues: [P0[], P1[], P2[], P3[], P4[]], +): Promise<void>; + +/** + * Uses combinatorics to test the behavior of a test function by comparing it to + * the expected behavior (defined by a reference function) for a large number of + * possible input values. + * + * First generates test cases by taking the cartesian product of the given + * values. Each test case is a set of N values corresponding to the N arguments + * for the test func and the reference func. For each test case, first the + * reference function will be called to obtain an "expected result", or if the + * reference function throws/rejects, an "expected error". Next, the test + * function will be called to obtain an "actual result", or if the test function + * throws/rejects, an "actual error". Each test case passes if at least one of + * the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * The first test case which does not meet one of these conditions will cause + * the entire test to fail and this function will throw/reject. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or "expected error" for a given test case. + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or "actual error" for a given test case. + * @param values an array of N arrays. Each inner array is a set of possible + * values which are passed into both the reference function and the test + * function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFuncAsync: (...args: any[]) => Promise<any>, + testFuncAsync: (...args: any[]) => Promise<any>, + allValues: any[], +): Promise<void> { + const testCases = combinatorics.cartesianProduct(...allValues); + let counter = 0; + testCases.forEach(async testCase => { + counter += 1; + it(`${name} ${counter}/${testCases.length}`, async () => { + await testWithReferenceFuncAsync(referenceFuncAsync, testFuncAsync, testCase as any); + }); + }); +} diff --git a/packages/contracts/test/utils/constants.ts b/packages/contracts/test/utils/constants.ts new file mode 100644 index 000000000..65eaee398 --- /dev/null +++ b/packages/contracts/test/utils/constants.ts @@ -0,0 +1,54 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +const TESTRPC_PRIVATE_KEYS_STRINGS = [ + '0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', + '0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72', + '0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1', + '0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0', + '0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249', + '0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd', + '0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f', + '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', + '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', + '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', +]; + +export const constants = { + BASE_16: 16, + INVALID_OPCODE: 'invalid opcode', + TESTRPC_NETWORK_ID: 50, + // Note(albrow): In practice V8 and most other engines limit the minimum + // interval for setInterval to 10ms. We still set it to 0 here in order to + // ensure we always use the minimum interval. + AWAIT_TRANSACTION_MINED_MS: 0, + MAX_ETHERTOKEN_WITHDRAW_GAS: 43000, + MAX_EXECUTE_TRANSACTION_GAS: 1000000, + MAX_TOKEN_TRANSFERFROM_GAS: 80000, + MAX_TOKEN_APPROVE_GAS: 60000, + MAX_TRANSFER_FROM_GAS: 150000, + DUMMY_TOKEN_NAME: '', + DUMMY_TOKEN_SYMBOL: '', + DUMMY_TOKEN_DECIMALS: new BigNumber(18), + DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(0), + NULL_BYTES: '0x', + NUM_DUMMY_ERC20_TO_DEPLOY: 3, + NUM_DUMMY_ERC721_TO_DEPLOY: 1, + NUM_ERC721_TOKENS_TO_MINT: 2, + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)), + INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + STATIC_ORDER_PARAMS: { + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + }, + WORD_LENGTH: 32, + ZERO_AMOUNT: new BigNumber(0), + PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18), +}; diff --git a/packages/contracts/test/utils/coverage.ts b/packages/contracts/test/utils/coverage.ts new file mode 100644 index 000000000..de29a3ecc --- /dev/null +++ b/packages/contracts/test/utils/coverage.ts @@ -0,0 +1,21 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { CoverageSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let coverageSubprovider: CoverageSubprovider; + +export const coverage = { + getCoverageSubproviderSingleton(): CoverageSubprovider { + if (_.isUndefined(coverageSubprovider)) { + coverageSubprovider = coverage._getCoverageSubprovider(); + } + return coverageSubprovider; + }, + _getCoverageSubprovider(): CoverageSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new CoverageSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/deployer.ts b/packages/contracts/test/utils/deployer.ts deleted file mode 100644 index 1baa47f09..000000000 --- a/packages/contracts/test/utils/deployer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Deployer } from '@0xproject/deployer'; -import { devConstants } from '@0xproject/dev-utils'; -import * as path from 'path'; - -import { constants } from '../../util/constants'; - -import { web3 } from './web3_wrapper'; - -const deployerOpts = { - web3Provider: web3.currentProvider, - artifactsDir: path.resolve('src', 'artifacts'), - jsonrpcUrl: devConstants.RPC_URL, - networkId: constants.TESTRPC_NETWORK_ID, - defaults: { - gas: devConstants.GAS_ESTIMATE, - }, -}; - -export const deployer = new Deployer(deployerOpts); diff --git a/packages/contracts/test/utils/erc20_wrapper.ts b/packages/contracts/test/utils/erc20_wrapper.ts new file mode 100644 index 000000000..424aae579 --- /dev/null +++ b/packages/contracts/test/utils/erc20_wrapper.ts @@ -0,0 +1,175 @@ +import { assetDataUtils } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; +import { ERC20BalancesByOwner } from './types'; +import { txDefaults } from './web3_wrapper'; + +export class ERC20Wrapper { + private readonly _tokenOwnerAddresses: string[]; + private readonly _contractOwnerAddress: string; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _provider: Provider; + private readonly _dummyTokenContracts: DummyERC20TokenContract[]; + private _proxyContract?: ERC20ProxyContract; + private _proxyIdIfExists?: string; + constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { + this._dummyTokenContracts = []; + this._web3Wrapper = new Web3Wrapper(provider); + this._provider = provider; + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + } + public async deployDummyTokensAsync( + numberToDeploy: number, + decimals: BigNumber, + ): Promise<DummyERC20TokenContract[]> { + for (let i = 0; i < numberToDeploy; i++) { + this._dummyTokenContracts.push( + await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + this._provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + decimals, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ), + ); + } + return this._dummyTokenContracts; + } + public async deployProxyAsync(): Promise<ERC20ProxyContract> { + this._proxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC20Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + public async setBalancesAndAllowancesAsync(): Promise<void> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + await this._web3Wrapper.awaitTransactionSuccessAsync( + await dummyTokenContract.setBalance.sendTransactionAsync( + tokenOwnerAddress, + constants.INITIAL_ERC20_BALANCE, + { from: this._contractOwnerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await dummyTokenContract.approve.sendTransactionAsync( + (this._proxyContract as ERC20ProxyContract).address, + constants.INITIAL_ERC20_ALLOWANCE, + { from: tokenOwnerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + } + } + public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const balance = new BigNumber(await tokenContract.balanceOf.callAsync(userAddress)); + return balance; + } + public async setBalanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.setBalance.sendTransactionAsync(userAddress, amount, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const proxyAddress = (this._proxyContract as ERC20ProxyContract).address; + const allowance = new BigNumber(await tokenContract.allowance.callAsync(userAddress, proxyAddress)); + return allowance; + } + public async setAllowanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const proxyAddress = (this._proxyContract as ERC20ProxyContract).address; + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.approve.sendTransactionAsync(proxyAddress, amount, { + from: userAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async getBalancesAsync(): Promise<ERC20BalancesByOwner> { + this._validateDummyTokenContractsExistOrThrow(); + const balancesByOwner: ERC20BalancesByOwner = {}; + const balances: BigNumber[] = []; + const balanceInfo: Array<{ tokenOwnerAddress: string; tokenAddress: string }> = []; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + balances.push(await dummyTokenContract.balanceOf.callAsync(tokenOwnerAddress)); + balanceInfo.push({ + tokenOwnerAddress, + tokenAddress: dummyTokenContract.address, + }); + } + } + _.forEach(balances, (balance, balanceIndex) => { + const tokenAddress = balanceInfo[balanceIndex].tokenAddress; + const tokenOwnerAddress = balanceInfo[balanceIndex].tokenOwnerAddress; + if (_.isUndefined(balancesByOwner[tokenOwnerAddress])) { + balancesByOwner[tokenOwnerAddress] = {}; + } + const wrappedBalance = new BigNumber(balance); + balancesByOwner[tokenOwnerAddress][tokenAddress] = wrappedBalance; + }); + return balancesByOwner; + } + public addDummyTokenContract(dummy: DummyERC20TokenContract): void { + if (!_.isUndefined(this._dummyTokenContracts)) { + this._dummyTokenContracts.push(dummy); + } + } + public addTokenOwnerAddress(address: string): void { + this._tokenOwnerAddresses.push(address); + } + public getTokenOwnerAddresses(): string[] { + return this._tokenOwnerAddresses; + } + public getTokenAddresses(): string[] { + const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address); + return tokenAddresses; + } + private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract { + const erc20ProxyData = assetDataUtils.decodeERC20AssetData(assetData); + const tokenAddress = erc20ProxyData.tokenAddress; + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + return tokenContractIfExists; + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenContracts)) { + throw new Error('Dummy ERC20 tokens not yet deployed, please call "deployDummyTokensAsync"'); + } + } + private _validateProxyContractExistsOrThrow(): void { + if (_.isUndefined(this._proxyContract)) { + throw new Error('ERC20 proxy contract not yet deployed, please call "deployProxyAsync"'); + } + } +} diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts new file mode 100644 index 000000000..d023b4d02 --- /dev/null +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -0,0 +1,236 @@ +import { generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; +import { ERC721TokenIdsByOwner } from './types'; +import { txDefaults } from './web3_wrapper'; + +export class ERC721Wrapper { + private readonly _tokenOwnerAddresses: string[]; + private readonly _contractOwnerAddress: string; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _provider: Provider; + private readonly _dummyTokenContracts: DummyERC721TokenContract[]; + private _proxyContract?: ERC721ProxyContract; + private _proxyIdIfExists?: string; + private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {}; + constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { + this._web3Wrapper = new Web3Wrapper(provider); + this._provider = provider; + this._dummyTokenContracts = []; + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + } + public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> { + for (let i = 0; i < constants.NUM_DUMMY_ERC721_TO_DEPLOY; i++) { + this._dummyTokenContracts.push( + await DummyERC721TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Token, + this._provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ), + ); + } + return this._dummyTokenContracts; + } + public async deployProxyAsync(): Promise<ERC721ProxyContract> { + this._proxyContract = await ERC721ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC721Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + public async setBalancesAndAllowancesAsync(): Promise<void> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + this._initialTokenIdsByOwner = {}; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + for (let i = 0; i < constants.NUM_ERC721_TOKENS_TO_MINT; i++) { + const tokenId = generatePseudoRandomSalt(); + await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); + if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) { + this._initialTokenIdsByOwner[tokenOwnerAddress] = { + [dummyTokenContract.address]: [], + }; + } + if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address])) { + this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = []; + } + this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId); + + await this.approveProxyAsync(dummyTokenContract.address, tokenId); + } + } + } + } + public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const doesExist = await tokenContract.exists.callAsync(tokenId); + return doesExist; + } + public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise<void> { + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + await this.approveAsync(proxyAddress, tokenAddress, tokenId); + } + public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId); + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.setApprovalForAll.sendTransactionAsync(proxyAddress, isApproved, { + from: tokenOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async approveAsync(to: string, tokenAddress: string, tokenId: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.approve.sendTransactionAsync(to, tokenId, { + from: tokenOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async transferFromAsync( + tokenAddress: string, + tokenId: BigNumber, + currentOwner: string, + userAddress: string, + ): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.transferFrom.sendTransactionAsync(currentOwner, userAddress, tokenId, { + from: currentOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async mintAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.mint.sendTransactionAsync(userAddress, tokenId, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async burnAsync(tokenAddress: string, tokenId: BigNumber, owner: string): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.burn.sendTransactionAsync(owner, tokenId, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async ownerOfAsync(tokenAddress: string, tokenId: BigNumber): Promise<string> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const owner = await tokenContract.ownerOf.callAsync(tokenId); + return owner; + } + public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId); + const isOwner = tokenOwner === userAddress; + return isOwner; + } + public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise<boolean> { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const operator = (this._proxyContract as ERC721ProxyContract).address; + const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator); + return didApproveAll; + } + public async isProxyApprovedAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const approvedAddress = await tokenContract.getApproved.callAsync(tokenId); + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + const isProxyAnApprovedOperator = approvedAddress === proxyAddress; + return isProxyAnApprovedOperator; + } + public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateBalancesAndAllowancesSetOrThrow(); + const tokenIdsByOwner: ERC721TokenIdsByOwner = {}; + const tokenOwnerAddresses: string[] = []; + const tokenInfo: Array<{ tokenId: BigNumber; tokenAddress: string }> = []; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + const initialTokenOwnerIds = this._initialTokenIdsByOwner[tokenOwnerAddress][ + dummyTokenContract.address + ]; + for (const tokenId of initialTokenOwnerIds) { + tokenOwnerAddresses.push(await dummyTokenContract.ownerOf.callAsync(tokenId)); + tokenInfo.push({ + tokenId, + tokenAddress: dummyTokenContract.address, + }); + } + } + } + _.forEach(tokenOwnerAddresses, (tokenOwnerAddress, ownerIndex) => { + const tokenAddress = tokenInfo[ownerIndex].tokenAddress; + const tokenId = tokenInfo[ownerIndex].tokenId; + if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress])) { + tokenIdsByOwner[tokenOwnerAddress] = { + [tokenAddress]: [], + }; + } + if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress][tokenAddress])) { + tokenIdsByOwner[tokenOwnerAddress][tokenAddress] = []; + } + tokenIdsByOwner[tokenOwnerAddress][tokenAddress].push(tokenId); + }); + return tokenIdsByOwner; + } + public getTokenOwnerAddresses(): string[] { + return this._tokenOwnerAddresses; + } + public getTokenAddresses(): string[] { + const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address); + return tokenAddresses; + } + private _getTokenContractFromAssetData(tokenAddress: string): DummyERC721TokenContract { + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + return tokenContractIfExists; + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenContracts)) { + throw new Error('Dummy ERC721 tokens not yet deployed, please call "deployDummyTokensAsync"'); + } + } + private _validateProxyContractExistsOrThrow(): void { + if (_.isUndefined(this._proxyContract)) { + throw new Error('ERC721 proxy contract not yet deployed, please call "deployProxyAsync"'); + } + } + private _validateBalancesAndAllowancesSetOrThrow(): void { + if (_.keys(this._initialTokenIdsByOwner).length === 0) { + throw new Error( + 'Dummy ERC721 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"', + ); + } + } +} diff --git a/packages/contracts/test/utils/exchange_wrapper.ts b/packages/contracts/test/utils/exchange_wrapper.ts new file mode 100644 index 000000000..d57592d6d --- /dev/null +++ b/packages/contracts/test/utils/exchange_wrapper.ts @@ -0,0 +1,269 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; + +import { formatters } from './formatters'; +import { LogDecoder } from './log_decoder'; +import { orderUtils } from './order_utils'; +import { FillResults, OrderInfo, SignedTransaction } from './types'; + +export class ExchangeWrapper { + private readonly _exchange: ExchangeContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor(exchangeContract: ExchangeContract, provider: Provider) { + this._exchange = exchangeContract; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, this._exchange.address); + } + public async fillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrder.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return txReceipt; + } + public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createCancel(signedOrder); + const txHash = await this._exchange.cancelOrder.sendTransactionAsync(params.order, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async fillOrKillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async fillOrderNoThrowAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrderNoThrow.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrKillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrdersNoThrow.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketSellOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const txHash = await this._exchange.marketSellOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmount, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketSellOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const txHash = await this._exchange.marketSellOrdersNoThrow.sendTransactionAsync( + params.orders, + params.takerAssetFillAmount, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const txHash = await this._exchange.marketBuyOrders.sendTransactionAsync( + params.orders, + params.makerAssetFillAmount, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const txHash = await this._exchange.marketBuyOrdersNoThrow.sendTransactionAsync( + params.orders, + params.makerAssetFillAmount, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchCancelOrdersAsync( + orders: SignedOrder[], + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchCancel(orders); + const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync(params.orders, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.cancelOrdersUpTo.sendTransactionAsync(salt, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async registerAssetProxyAsync( + assetProxyAddress: string, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.registerAssetProxy.sendTransactionAsync(assetProxyAddress, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeTransactionAsync( + signedTx: SignedTransaction, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.executeTransaction.sendTransactionAsync( + signedTx.salt, + signedTx.signerAddress, + signedTx.data, + signedTx.signature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> { + const filledAmount = new BigNumber(await this._exchange.filled.callAsync(orderHashHex)); + return filledAmount; + } + public async isCancelledAsync(orderHashHex: string): Promise<boolean> { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { + const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; + return orderInfo; + } + public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> { + const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; + return ordersInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const txHash = await this._exchange.matchOrders.sendTransactionAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getFillOrderResultsAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<FillResults> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const fillResults = await this._exchange.fillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + return fillResults; + } + public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + return data; + } +} diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts new file mode 100644 index 000000000..284c4a2db --- /dev/null +++ b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts @@ -0,0 +1,913 @@ +import { + assetDataUtils, + BalanceAndProxyAllowanceLazyStore, + ExchangeTransferSimulator, + orderHashUtils, + OrderStateUtils, + OrderValidationUtils, +} from '@0xproject/order-utils'; +import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber, errorUtils, logUtils } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types'; +import * as _ from 'lodash'; +import 'make-promises-safe'; + +import { ExchangeContract, ExchangeFillEventArgs } from '../../generated_contract_wrappers/exchange'; +import { TestLibsContract } from '../../generated_contract_wrappers/test_libs'; + +import { artifacts } from './artifacts'; +import { expectTransactionFailedAsync } from './assertions'; +import { AssetWrapper } from './asset_wrapper'; +import { chaiSetup } from './chai_setup'; +import { constants } from './constants'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; +import { ExchangeWrapper } from './exchange_wrapper'; +import { OrderFactoryFromScenario } from './order_factory_from_scenario'; +import { orderUtils } from './order_utils'; +import { signingUtils } from './signing_utils'; +import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher'; +import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher'; +import { + AllowanceAmountScenario, + AssetDataScenario, + BalanceAmountScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + FillScenario, + OrderAssetAmountScenario, + TakerAssetFillAmountScenario, + TakerScenario, + TraderStateScenario, +} from './types'; + +chaiSetup.configure(); +const expect = chai.expect; + +/** + * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some + * required async setup, a factory method is required. + * @param web3Wrapper Web3Wrapper instance + * @param txDefaults Default Ethereum tx options + * @return FillOrderCombinatorialUtils instance + */ +export async function fillOrderCombinatorialUtilsFactoryAsync( + web3Wrapper: Web3Wrapper, + txDefaults: Partial<TxData>, +): Promise<FillOrderCombinatorialUtils> { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const userAddresses = _.slice(accounts, 0, 5); + const [ownerAddress, makerAddress, takerAddress] = userAddresses; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; + + const provider = web3Wrapper.getProvider(); + const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress); + const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress); + + const erc20EighteenDecimalTokenCount = 3; + const eighteenDecimals = new BigNumber(18); + const [ + erc20EighteenDecimalTokenA, + erc20EighteenDecimalTokenB, + zrxToken, + ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals); + const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + + const erc20FiveDecimalTokenCount = 2; + const fiveDecimals = new BigNumber(5); + const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( + erc20FiveDecimalTokenCount, + fiveDecimals, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + + const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]); + + const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress); + + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const orderFactory = new OrderFactoryFromScenario( + userAddresses, + zrxToken.address, + [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address], + [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address], + erc721Token, + erc721Balances, + exchangeContract.address, + ); + + const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(artifacts.TestLibs, provider, txDefaults); + + const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils( + orderFactory, + ownerAddress, + makerAddress, + makerPrivateKey, + takerAddress, + zrxAssetData, + exchangeWrapper, + assetWrapper, + testLibsContract, + ); + return fillOrderCombinatorialUtils; +} + +export class FillOrderCombinatorialUtils { + public orderFactory: OrderFactoryFromScenario; + public ownerAddress: string; + public makerAddress: string; + public makerPrivateKey: Buffer; + public takerAddress: string; + public zrxAssetData: string; + public exchangeWrapper: ExchangeWrapper; + public assetWrapper: AssetWrapper; + public testLibsContract: TestLibsContract; + public static generateFillOrderCombinations(): FillScenario[] { + const takerScenarios = [ + TakerScenario.Unspecified, + // TakerScenario.CorrectlySpecified, + // TakerScenario.IncorrectlySpecified, + ]; + const feeRecipientScenarios = [ + FeeRecipientAddressScenario.EthUserAddress, + // FeeRecipientAddressScenario.BurnAddress, + ]; + const makerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const takerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const makerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const takerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const expirationTimeSecondsScenario = [ + ExpirationTimeSecondsScenario.InFuture, + ExpirationTimeSecondsScenario.InPast, + ]; + const makerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetFillAmountScenario = [ + TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount, + ]; + const makerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const makerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([ + takerScenarios, + feeRecipientScenarios, + makerAssetAmountScenario, + takerAssetAmountScenario, + makerFeeScenario, + takerFeeScenario, + expirationTimeSecondsScenario, + makerAssetDataScenario, + takerAssetDataScenario, + takerAssetFillAmountScenario, + makerAssetBalanceScenario, + makerAssetAllowanceScenario, + makerZRXBalanceScenario, + makerZRXAllowanceScenario, + takerAssetBalanceScenario, + takerAssetAllowanceScenario, + takerZRXBalanceScenario, + takerZRXAllowanceScenario, + ]); + + const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => { + // tslint:disable:custom-no-magic-numbers + const fillScenario: FillScenario = { + orderScenario: { + takerScenario: fillScenarioArray[0] as TakerScenario, + feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario, + makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario, + takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario, + makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario, + takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario, + expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario, + makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario, + takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario, + }, + takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario, + makerStateScenario: { + traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario, + }, + takerStateScenario: { + traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario, + }, + }; + // tslint:enable:custom-no-magic-numbers + return fillScenario; + }); + + return fillScenarios; + } + /** + * Recursive implementation of generating all combinations of the supplied + * string-containing arrays. + */ + private static _getAllCombinations(arrays: string[][]): string[][] { + // Base case + if (arrays.length === 1) { + const remainingValues = _.map(arrays[0], val => { + return [val]; + }); + return remainingValues; + } else { + const result = []; + const restOfArrays = arrays.slice(1); + const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array + // tslint:disable:prefer-for-of + for (let i = 0; i < allCombinationsOfRemaining.length; i++) { + for (let j = 0; j < arrays[0].length; j++) { + result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]); + } + } + // tslint:enable:prefer-for-of + return result; + } + } + constructor( + orderFactory: OrderFactoryFromScenario, + ownerAddress: string, + makerAddress: string, + makerPrivateKey: Buffer, + takerAddress: string, + zrxAssetData: string, + exchangeWrapper: ExchangeWrapper, + assetWrapper: AssetWrapper, + testLibsContract: TestLibsContract, + ) { + this.orderFactory = orderFactory; + this.ownerAddress = ownerAddress; + this.makerAddress = makerAddress; + this.makerPrivateKey = makerPrivateKey; + this.takerAddress = takerAddress; + this.zrxAssetData = zrxAssetData; + this.exchangeWrapper = exchangeWrapper; + this.assetWrapper = assetWrapper; + this.testLibsContract = testLibsContract; + } + public async testFillOrderScenarioAsync( + provider: Provider, + fillScenario: FillScenario, + isVerbose: boolean = false, + ): Promise<void> { + // 1. Generate order + const order = this.orderFactory.generateOrder(fillScenario.orderScenario); + + // 2. Sign order + const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); + const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + + const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper); + const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher( + this.exchangeWrapper, + this.zrxAssetData, + ); + + // 3. Figure out fill amount + const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync( + signedOrder, + fillScenario.takerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher, + ); + + // 4. Permutate the maker and taker balance/allowance scenarios + await this._modifyTraderStateAsync( + fillScenario.makerStateScenario, + fillScenario.takerStateScenario, + signedOrder, + takerAssetFillAmount, + ); + + // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected? + const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher); + const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher); + const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore); + + let fillRevertReasonIfExists; + try { + await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTransferSimulator, + provider, + signedOrder, + takerAssetFillAmount, + this.takerAddress, + this.zrxAssetData, + ); + if (isVerbose) { + logUtils.log(`Expecting fillOrder to succeed.`); + } + } catch (err) { + fillRevertReasonIfExists = err.message; + if (isVerbose) { + logUtils.log(`Expecting fillOrder to fail with:`); + logUtils.log(err); + } + } + + // 6. Fill the order + await this._fillOrderAndAssertOutcomeAsync( + signedOrder, + takerAssetFillAmount, + lazyStore, + fillRevertReasonIfExists, + ); + + await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount); + } + private async _fillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + lazyStore: BalanceAndProxyAllowanceLazyStore, + fillRevertReasonIfExists: RevertReason | undefined, + ): Promise<void> { + if (!_.isUndefined(fillRevertReasonIfExists)) { + return expectTransactionFailedAsync( + this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }), + fillRevertReasonIfExists, + ); + } + + const makerAddress = signedOrder.makerAddress; + const makerAssetData = signedOrder.makerAssetData; + const takerAssetData = signedOrder.takerAssetData; + const feeRecipient = signedOrder.feeRecipientAddress; + + const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress); + const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress); + const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress); + const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress); + const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress); + const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress); + const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress); + const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress); + const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress); + const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync( + this.zrxAssetData, + this.takerAddress, + ); + const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient); + + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount); + const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill) + ? remainingTakerAmountToFill + : alreadyFilledTakerAmount.add(takerAssetFillAmount); + + const expFilledMakerAmount = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + const expMakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + const expTakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + const fillResults = await this.exchangeWrapper.getFillOrderResultsAsync(signedOrder, this.takerAddress, { + takerAssetFillAmount, + }); + expect(fillResults.takerAssetFilledAmount).to.be.bignumber.equal( + expFilledTakerAmount, + 'takerAssetFilledAmount', + ); + expect(fillResults.makerAssetFilledAmount).to.be.bignumber.equal( + expFilledMakerAmount, + 'makerAssetFilledAmount', + ); + expect(fillResults.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'takerFeePaid'); + expect(fillResults.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'makerFeePaid'); + + // - Let's fill the order! + const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { + takerAssetFillAmount, + }); + + const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount'); + + expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); + // tslint:disable-next-line:no-unnecessary-type-assertion + const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>; + expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); + expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress'); + expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress'); + expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal( + expFilledMakerAmount, + 'log.args.makerAssetFilledAmount', + ); + expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal( + expFilledTakerAmount, + 'log.args.takerAssetFilledAmount', + ); + expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid'); + expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid'); + expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash'); + expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData'); + expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData'); + + const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData); + expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal( + expMakerAssetBalanceOfMaker, + 'makerAssetBalanceOfMaker', + ); + + const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + makerAssetData, + ); + expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal( + expMakerAssetAllowanceOfMaker, + 'makerAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData); + expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal( + expTakerAssetBalanceOfMaker, + 'takerAssetBalanceOfMaker', + ); + + const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker'); + + const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfMaker, + 'ZRXAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData); + expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal( + expTakerAssetBalanceOfTaker, + 'TakerAssetBalanceOfTaker', + ); + + const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + takerAssetData, + ); + + expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal( + expTakerAssetAllowanceOfTaker, + 'TakerAssetAllowanceOfTaker', + ); + + const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData); + expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal( + expMakerAssetBalanceOfTaker, + 'MakerAssetBalanceOfTaker', + ); + + const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker'); + + const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfTaker, + 'ZRXAssetAllowanceOfTaker', + ); + + const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync( + feeRecipient, + this.zrxAssetData, + ); + expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal( + expZRXAssetBalanceOfFeeRecipient, + 'ZRXAssetBalanceOfFeeRecipient', + ); + } + private async _abiEncodeFillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + ): Promise<void> { + const params = orderUtils.createFill(signedOrder, takerAssetFillAmount); + const expectedAbiEncodedData = this.exchangeWrapper.abiEncodeFillOrder(signedOrder, { takerAssetFillAmount }); + const libsAbiEncodedData = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + expect(libsAbiEncodedData).to.be.equal(expectedAbiEncodedData, 'ABIEncodedFillOrderData'); + } + private async _getTakerAssetFillAmountAsync( + signedOrder: SignedOrder, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher, + ): Promise<BigNumber> { + const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); + const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync( + signedOrder, + this.takerAddress, + ); + + let takerAssetFillAmount; + switch (takerAssetFillAmountScenario) { + case TakerAssetFillAmountScenario.Zero: + takerAssetFillAmount = new BigNumber(0); + break; + + case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount; + break; + + case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount.add(1); + break; + + case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount: + const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData); + const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData); + const isEitherAssetERC721 = + takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721; + if (isEitherAssetERC721) { + throw new Error( + 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.', + ); + } + takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor(); + break; + + default: + throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario); + } + + return takerAssetFillAmount; + } + private async _modifyTraderStateAsync( + makerStateScenario: TraderStateScenario, + takerStateScenario: TraderStateScenario, + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + ): Promise<void> { + const makerAssetFillAmount = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + switch (makerStateScenario.traderAssetBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (makerAssetFillAmount.eq(0)) { + throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`); + } + const tooLowBalance = makerAssetFillAmount.minus(1); + await this.assetWrapper.setBalanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + tooLowBalance, + ); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = makerAssetFillAmount; + await this.assetWrapper.setBalanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + exactBalance, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.traderAssetBalance', + makerStateScenario.traderAssetBalance, + ); + } + + const makerFee = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + switch (makerStateScenario.zrxFeeBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (makerFee.eq(0)) { + throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`); + } + const tooLowBalance = makerFee.minus(1); + await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = makerFee; + await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance); + } + + switch (makerStateScenario.traderAssetAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = makerAssetFillAmount.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = makerAssetFillAmount; + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.traderAssetAllowance', + makerStateScenario.traderAssetAllowance, + ); + } + + switch (makerStateScenario.zrxFeeAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = makerFee.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = makerFee; + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.zrxFeeAllowance', + makerStateScenario.zrxFeeAllowance, + ); + } + + switch (takerStateScenario.traderAssetBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (takerAssetFillAmount.eq(0)) { + throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`); + } + const tooLowBalance = takerAssetFillAmount.minus(1); + await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = takerAssetFillAmount; + await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.traderAssetBalance', + takerStateScenario.traderAssetBalance, + ); + } + + const takerFee = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + switch (takerStateScenario.zrxFeeBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (takerFee.eq(0)) { + throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`); + } + const tooLowBalance = takerFee.minus(1); + await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = takerFee; + await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance); + } + + switch (takerStateScenario.traderAssetAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = takerAssetFillAmount.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = takerAssetFillAmount; + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.traderAssetAllowance', + takerStateScenario.traderAssetAllowance, + ); + } + + switch (takerStateScenario.zrxFeeAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = takerFee.minus(1); + await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = takerFee; + await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.zrxFeeAllowance', + takerStateScenario.zrxFeeAllowance, + ); + } + } +} // tslint:disable:max-file-line-count diff --git a/packages/contracts/test/utils/formatters.ts b/packages/contracts/test/utils/formatters.ts new file mode 100644 index 000000000..32e4787d6 --- /dev/null +++ b/packages/contracts/test/utils/formatters.ts @@ -0,0 +1,68 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { orderUtils } from './order_utils'; +import { BatchCancelOrders, BatchFillOrders, MarketBuyOrders, MarketSellOrders } from './types'; + +export const formatters = { + createBatchFill(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[] = []): BatchFillOrders { + const batchFill: BatchFillOrders = { + orders: [], + signatures: [], + takerAssetFillAmounts, + }; + _.forEach(signedOrders, signedOrder => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + batchFill.orders.push(orderWithoutExchangeAddress); + batchFill.signatures.push(signedOrder.signature); + if (takerAssetFillAmounts.length < signedOrders.length) { + batchFill.takerAssetFillAmounts.push(signedOrder.takerAssetAmount); + } + }); + return batchFill; + }, + createMarketSellOrders(signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber): MarketSellOrders { + const marketSellOrders: MarketSellOrders = { + orders: [], + signatures: [], + takerAssetFillAmount, + }; + _.forEach(signedOrders, (signedOrder, i) => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + if (i !== 0) { + orderWithoutExchangeAddress.takerAssetData = constants.NULL_BYTES; + } + marketSellOrders.orders.push(orderWithoutExchangeAddress); + marketSellOrders.signatures.push(signedOrder.signature); + }); + return marketSellOrders; + }, + createMarketBuyOrders(signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber): MarketBuyOrders { + const marketBuyOrders: MarketBuyOrders = { + orders: [], + signatures: [], + makerAssetFillAmount, + }; + _.forEach(signedOrders, (signedOrder, i) => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + if (i !== 0) { + orderWithoutExchangeAddress.makerAssetData = constants.NULL_BYTES; + } + marketBuyOrders.orders.push(orderWithoutExchangeAddress); + marketBuyOrders.signatures.push(signedOrder.signature); + }); + return marketBuyOrders; + }, + createBatchCancel(signedOrders: SignedOrder[]): BatchCancelOrders { + const batchCancel: BatchCancelOrders = { + orders: [], + }; + _.forEach(signedOrders, signedOrder => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + batchCancel.orders.push(orderWithoutExchangeAddress); + }); + return batchCancel; + }, +}; diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts new file mode 100644 index 000000000..5b9a63ddf --- /dev/null +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -0,0 +1,118 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; + +import { constants } from './constants'; +import { formatters } from './formatters'; +import { LogDecoder } from './log_decoder'; +import { MarketSellOrders } from './types'; + +export class ForwarderWrapper { + private readonly _web3Wrapper: Web3Wrapper; + private readonly _forwarderContract: ForwarderContract; + private readonly _logDecoder: LogDecoder; + public static getPercentageOfValue(value: BigNumber, percentage: number): BigNumber { + const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100); + const newValue = value.times(numerator).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR); + return newValue; + } + public static getWethForFeeOrders(feeAmount: BigNumber, feeOrders: SignedOrder[]): BigNumber { + let wethAmount = new BigNumber(0); + let remainingFeeAmount = feeAmount; + _.forEach(feeOrders, feeOrder => { + const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); + if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { + wethAmount = wethAmount + .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) + .plus(1); + remainingFeeAmount = new BigNumber(0); + } else if (!remainingFeeAmount.isZero()) { + wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); + remainingFeeAmount = remainingFeeAmount.minus(feeAvailable); + } + }); + return wethAmount; + } + private static _createOptimizedOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, (signedOrder, index) => { + signedOrder.takerAssetData = constants.NULL_BYTES; + if (index > 0) { + signedOrder.makerAssetData = constants.NULL_BYTES; + } + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + private static _createOptimizedZrxOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, signedOrder => { + signedOrder.makerAssetData = constants.NULL_BYTES; + signedOrder.takerAssetData = constants.NULL_BYTES; + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + constructor(contractInstance: ForwarderContract, provider: Provider) { + this._forwarderContract = contractInstance; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address); + } + public async marketSellOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketSellOrdersWithEth.sendTransactionAsync( + params.orders, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + makerAssetFillAmount: BigNumber, + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( + params.orders, + makerAssetFillAmount, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async withdrawAssetAsync( + assetData: string, + amount: BigNumber, + txData: TxDataPayable, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._forwarderContract.withdrawAsset.sendTransactionAsync(assetData, amount, txData); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/packages/contracts/test/utils/log_decoder.ts b/packages/contracts/test/utils/log_decoder.ts new file mode 100644 index 000000000..5a4801319 --- /dev/null +++ b/packages/contracts/test/utils/log_decoder.ts @@ -0,0 +1,55 @@ +import { ContractArtifact } from '@0xproject/sol-compiler'; +import { AbiDecoder, BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { + AbiDefinition, + DecodedLogArgs, + LogEntry, + LogWithDecodedArgs, + RawLog, + TransactionReceiptWithDecodedLogs, +} from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; + +export class LogDecoder { + private readonly _web3Wrapper: Web3Wrapper; + private readonly _contractAddress: string; + private readonly _abiDecoder: AbiDecoder; + public static wrapLogBigNumbers(log: any): any { + const argNames = _.keys(log.args); + for (const argName of argNames) { + const isWeb3BigNumber = _.startsWith(log.args[argName].constructor.toString(), 'function BigNumber('); + if (isWeb3BigNumber) { + log.args[argName] = new BigNumber(log.args[argName]); + } + } + } + constructor(web3Wrapper: Web3Wrapper, contractAddress: string) { + this._web3Wrapper = web3Wrapper; + this._contractAddress = contractAddress; + const abiArrays: AbiDefinition[][] = []; + _.forEach(artifacts, (artifact: ContractArtifact) => { + const compilerOutput = artifact.compilerOutput; + abiArrays.push(compilerOutput.abi); + }); + this._abiDecoder = new AbiDecoder(abiArrays); + } + public decodeLogOrThrow<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog { + const logWithDecodedArgsOrLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + // tslint:disable-next-line:no-unnecessary-type-assertion + if (_.isUndefined((logWithDecodedArgsOrLog as LogWithDecodedArgs<ArgsType>).args)) { + throw new Error(`Unable to decode log: ${JSON.stringify(log)}`); + } + LogDecoder.wrapLogBigNumbers(logWithDecodedArgsOrLog); + return logWithDecodedArgsOrLog; + } + public async getTxWithDecodedLogsAsync(txHash: string): Promise<TransactionReceiptWithDecodedLogs> { + const tx = await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + tx.logs = _.filter(tx.logs, log => log.address === this._contractAddress); + tx.logs = _.map(tx.logs, log => this.decodeLogOrThrow(log)); + return tx; + } +} diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/test/utils/match_order_tester.ts new file mode 100644 index 000000000..fa2eabc12 --- /dev/null +++ b/packages/contracts/test/utils/match_order_tester.ts @@ -0,0 +1,326 @@ +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { AssetProxyId, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { chaiSetup } from './chai_setup'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; +import { ExchangeWrapper } from './exchange_wrapper'; +import { ERC20BalancesByOwner, ERC721TokenIdsByOwner, TransferAmountsByMatchOrders as TransferAmounts } from './types'; + +chaiSetup.configure(); +const expect = chai.expect; + +export class MatchOrderTester { + private readonly _exchangeWrapper: ExchangeWrapper; + private readonly _erc20Wrapper: ERC20Wrapper; + private readonly _erc721Wrapper: ERC721Wrapper; + private readonly _feeTokenAddress: string; + + /// @dev Compares a pair of ERC20 balances and a pair of ERC721 token owners. + /// @param expectedNewERC20BalancesByOwner Expected ERC20 balances. + /// @param realERC20BalancesByOwner Actual ERC20 balances. + /// @param expectedNewERC721TokenIdsByOwner Expected ERC721 token owners. + /// @param realERC721TokenIdsByOwner Actual ERC20 token owners. + /// @return True only if ERC20 balances match and ERC721 token owners match. + private static _compareExpectedAndRealBalances( + expectedNewERC20BalancesByOwner: ERC20BalancesByOwner, + realERC20BalancesByOwner: ERC20BalancesByOwner, + expectedNewERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + realERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + ): boolean { + // ERC20 Balances + const doesErc20BalancesMatch = _.isEqual(expectedNewERC20BalancesByOwner, realERC20BalancesByOwner); + if (!doesErc20BalancesMatch) { + return false; + } + // ERC721 Token Ids + const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues( + expectedNewERC721TokenIdsByOwner, + tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }, + ); + const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }); + const doesErc721TokenIdsMatch = _.isEqual( + sortedExpectedNewERC721TokenIdsByOwner, + sortedNewERC721TokenIdsByOwner, + ); + return doesErc721TokenIdsMatch; + } + /// @dev Constructs new MatchOrderTester. + /// @param exchangeWrapper Used to call to the Exchange. + /// @param erc20Wrapper Used to fetch ERC20 balances. + /// @param erc721Wrapper Used to fetch ERC721 token owners. + /// @param feeTokenAddress Address of ERC20 fee token. + constructor( + exchangeWrapper: ExchangeWrapper, + erc20Wrapper: ERC20Wrapper, + erc721Wrapper: ERC721Wrapper, + feeTokenAddress: string, + ) { + this._exchangeWrapper = exchangeWrapper; + this._erc20Wrapper = erc20Wrapper; + this._erc721Wrapper = erc721Wrapper; + this._feeTokenAddress = feeTokenAddress; + } + /// @dev Matches two complementary orders and validates results. + /// Validation either succeeds or throws. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param initialTakerAssetFilledAmountLeft Current amount the left order has been filled. + /// @param initialTakerAssetFilledAmountRight Current amount the right order has been filled. + /// @return New ERC20 balances & ERC721 token owners. + public async matchOrdersAndVerifyBalancesAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + initialTakerAssetFilledAmountLeft?: BigNumber, + initialTakerAssetFilledAmountRight?: BigNumber, + ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> { + // Verify Left order preconditions + const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedOrderFilledAmountLeft = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(expectedOrderFilledAmountLeft).to.be.bignumber.equal(orderTakerAssetFilledAmountLeft); + // Verify Right order preconditions + const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderRight), + ); + const expectedOrderFilledAmountRight = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(expectedOrderFilledAmountRight).to.be.bignumber.equal(orderTakerAssetFilledAmountRight); + // Match left & right orders + await this._exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync(); + const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync(); + // Calculate expected balance changes + const expectedTransferAmounts = await this._calculateExpectedTransferAmountsAsync( + signedOrderLeft, + signedOrderRight, + orderTakerAssetFilledAmountLeft, + orderTakerAssetFilledAmountRight, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + expectedTransferAmounts, + ); + // Assert our expected balances are equal to the actual balances + const didExpectedBalancesMatchRealBalances = MatchOrderTester._compareExpectedAndRealBalances( + expectedERC20BalancesByOwner, + newERC20BalancesByOwner, + expectedERC721TokenIdsByOwner, + newERC721TokenIdsByOwner, + ); + expect(didExpectedBalancesMatchRealBalances).to.be.true(); + return [newERC20BalancesByOwner, newERC721TokenIdsByOwner]; + } + /// @dev Calculates expected transfer amounts between order makers, fee recipients, and + /// the taker when two orders are matched. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param orderTakerAssetFilledAmountLeft How much left order has been filled, prior to matching orders. + /// @param orderTakerAssetFilledAmountRight How much the right order has been filled, prior to matching orders. + /// @return TransferAmounts A struct containing the expected transfer amounts. + private async _calculateExpectedTransferAmountsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + orderTakerAssetFilledAmountLeft: BigNumber, + orderTakerAssetFilledAmountRight: BigNumber, + ): Promise<TransferAmounts> { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(orderTakerAssetFilledAmountLeft); + const amountSoldByLeftMaker = amountBoughtByLeftMaker + .times(signedOrderLeft.makerAssetAmount) + .dividedToIntegerBy(signedOrderLeft.takerAssetAmount); + const amountReceivedByRightMaker = amountBoughtByLeftMaker + .times(signedOrderRight.takerAssetAmount) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountReceivedByRightMaker); + let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderRight), + ); + amountBoughtByRightMaker = amountBoughtByRightMaker.minus(orderTakerAssetFilledAmountRight); + const amountSoldByRightMaker = amountBoughtByRightMaker + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + const amountReceivedByLeftMaker = amountSoldByRightMaker; + const feePaidByLeftMaker = signedOrderLeft.makerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByRightMaker = signedOrderRight.makerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const feePaidByTakerLeft = signedOrderLeft.takerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByTakerRight = signedOrderRight.takerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const totalFeePaidByTaker = feePaidByTakerLeft.add(feePaidByTakerRight); + const feeReceivedLeft = feePaidByLeftMaker.add(feePaidByTakerLeft); + const feeReceivedRight = feePaidByRightMaker.add(feePaidByTakerRight); + // Return values + const expectedTransferAmounts = { + // Left Maker + amountBoughtByLeftMaker, + amountSoldByLeftMaker, + amountReceivedByLeftMaker, + feePaidByLeftMaker, + // Right Maker + amountBoughtByRightMaker, + amountSoldByRightMaker, + amountReceivedByRightMaker, + feePaidByRightMaker, + // Taker + amountReceivedByTaker, + feePaidByTakerLeft, + feePaidByTakerRight, + totalFeePaidByTaker, + // Fee Recipients + feeReceivedLeft, + feeReceivedRight, + }; + return expectedTransferAmounts; + } + /// @dev Calculates the expected balances of order makers, fee recipients, and the taker, + /// as a result of matching two orders. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param expectedTransferAmounts A struct containing the expected transfer amounts. + /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched. + private _calculateExpectedBalances( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + expectedTransferAmounts: TransferAmounts, + ): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] { + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Operations are performed on copies of the balances + const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner); + const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner); + // Left Maker Asset (Right Taker Asset) + const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData); + if (makerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc20AssetData.tokenAddress; + const takerAssetAddressRight = makerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + takerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add( + expectedTransferAmounts.amountReceivedByRightMaker, + ); + // Taker + expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + takerAddress + ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker); + } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc721AssetData.tokenAddress; + const makerAssetIdLeft = erc721AssetData.tokenId; + const takerAssetAddressRight = makerAssetAddressLeft; + const takerAssetIdRight = makerAssetIdLeft; + // Left Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft); + // Right Maker + expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight); + // Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset. + } + // Left Taker Asset (Right Maker Asset) + // Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset. + const takerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.takerAssetData); + if (takerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.takerAssetData); + const takerAssetAddressLeft = erc20AssetData.tokenAddress; + const makerAssetAddressRight = takerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][takerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + makerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ); + } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData); + const makerAssetAddressRight = erc721AssetData.tokenAddress; + const makerAssetIdRight = erc721AssetData.tokenId; + const takerAssetAddressLeft = makerAssetAddressRight; + const takerAssetIdLeft = makerAssetIdRight; + // Right Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight); + // Left Maker + expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft); + } + // Left Maker Fees + expectedNewERC20BalancesByOwner[makerAddressLeft][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker); + // Right Maker Fees + expectedNewERC20BalancesByOwner[makerAddressRight][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressRight + ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker); + // Taker Fees + expectedNewERC20BalancesByOwner[takerAddress][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + takerAddress + ][this._feeTokenAddress].minus(expectedTransferAmounts.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedLeft, + ); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedRight, + ); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } +} diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/packages/contracts/test/utils/multi_sig_wrapper.ts new file mode 100644 index 000000000..8c8055d4a --- /dev/null +++ b/packages/contracts/test/utils/multi_sig_wrapper.ts @@ -0,0 +1,60 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AssetProxyOwnerContract } from '../../generated_contract_wrappers/asset_proxy_owner'; +import { MultiSigWalletContract } from '../../generated_contract_wrappers/multi_sig_wallet'; + +import { constants } from './constants'; +import { LogDecoder } from './log_decoder'; + +export class MultiSigWrapper { + private readonly _multiSig: MultiSigWalletContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor(multiSigContract: MultiSigWalletContract, provider: Provider) { + this._multiSig = multiSigContract; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, this._multiSig.address); + } + public async submitTransactionAsync( + destination: string, + data: string, + from: string, + opts: { value?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value; + const txHash = await this._multiSig.submitTransaction.sendTransactionAsync(destination, value, data, { + from, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._multiSig.confirmTransaction.sendTransactionAsync(txId, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._multiSig.executeTransaction.sendTransactionAsync(txId, { + from, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeRemoveAuthorizedAddressAtIndexAsync( + txId: BigNumber, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + // tslint:disable-next-line:no-unnecessary-type-assertion + const txHash = await (this + ._multiSig as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/packages/contracts/test/utils/order_factory.ts b/packages/contracts/test/utils/order_factory.ts new file mode 100644 index 000000000..63a893695 --- /dev/null +++ b/packages/contracts/test/utils/order_factory.ts @@ -0,0 +1,38 @@ +import { generatePseudoRandomSalt, orderHashUtils } from '@0xproject/order-utils'; +import { Order, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { getLatestBlockTimestampAsync } from './block_timestamp'; +import { constants } from './constants'; +import { signingUtils } from './signing_utils'; + +export class OrderFactory { + private readonly _defaultOrderParams: Partial<Order>; + private readonly _privateKey: Buffer; + constructor(privateKey: Buffer, defaultOrderParams: Partial<Order>) { + this._defaultOrderParams = defaultOrderParams; + this._privateKey = privateKey; + } + public async newSignedOrderAsync( + customOrderParams: Partial<Order> = {}, + signatureType: SignatureType = SignatureType.EthSign, + ): Promise<SignedOrder> { + const tenMinutesInSeconds = 10 * 60; + const currentBlockTimestamp = await getLatestBlockTimestampAsync(); + const order = ({ + senderAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: new BigNumber(currentBlockTimestamp).add(tenMinutesInSeconds), + salt: generatePseudoRandomSalt(), + takerAddress: constants.NULL_ADDRESS, + ...this._defaultOrderParams, + ...customOrderParams, + } as any) as Order; + const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); + const signature = signingUtils.signMessage(orderHashBuff, this._privateKey, signatureType); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + return signedOrder; + } +} diff --git a/packages/contracts/test/utils/order_factory_from_scenario.ts b/packages/contracts/test/utils/order_factory_from_scenario.ts new file mode 100644 index 000000000..a908140b9 --- /dev/null +++ b/packages/contracts/test/utils/order_factory_from_scenario.ts @@ -0,0 +1,273 @@ +import { assetDataUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { Order } from '@0xproject/types'; +import { BigNumber, errorUtils } from '@0xproject/utils'; + +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; + +import { constants } from './constants'; +import { + AssetDataScenario, + ERC721TokenIdsByOwner, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + OrderAssetAmountScenario, + OrderScenario, + TakerScenario, +} from './types'; + +const TEN_UNITS_EIGHTEEN_DECIMALS = new BigNumber(10_000_000_000_000_000_000); +const FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(5_000_000_000_000_000_000); +const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000); +const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000); +const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000); +const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000); +const ONE_NFT_UNIT = new BigNumber(1); + +export class OrderFactoryFromScenario { + private readonly _userAddresses: string[]; + private readonly _zrxAddress: string; + private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[]; + private readonly _erc20FiveDecimalTokenAddresses: string[]; + private readonly _erc721Token: DummyERC721TokenContract; + private readonly _erc721Balances: ERC721TokenIdsByOwner; + private readonly _exchangeAddress: string; + constructor( + userAddresses: string[], + zrxAddress: string, + nonZrxERC20EighteenDecimalTokenAddresses: string[], + erc20FiveDecimalTokenAddresses: string[], + erc721Token: DummyERC721TokenContract, + erc721Balances: ERC721TokenIdsByOwner, + exchangeAddress: string, + ) { + this._userAddresses = userAddresses; + this._zrxAddress = zrxAddress; + this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses; + this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses; + this._erc721Token = erc721Token; + this._erc721Balances = erc721Balances; + this._exchangeAddress = exchangeAddress; + } + public generateOrder(orderScenario: OrderScenario): Order { + const makerAddress = this._userAddresses[1]; + let takerAddress = this._userAddresses[2]; + const erc721MakerAssetIds = this._erc721Balances[makerAddress][this._erc721Token.address]; + const erc721TakerAssetIds = this._erc721Balances[takerAddress][this._erc721Token.address]; + let feeRecipientAddress; + let makerAssetAmount; + let takerAssetAmount; + let makerFee; + let takerFee; + let expirationTimeSeconds; + let makerAssetData; + let takerAssetData; + + switch (orderScenario.feeRecipientScenario) { + case FeeRecipientAddressScenario.BurnAddress: + feeRecipientAddress = constants.NULL_ADDRESS; + break; + case FeeRecipientAddressScenario.EthUserAddress: + feeRecipientAddress = this._userAddresses[4]; + break; + default: + throw errorUtils.spawnSwitchErr('FeeRecipientAddressScenario', orderScenario.feeRecipientScenario); + } + + switch (orderScenario.makerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + makerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress); + break; + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + makerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[0]); + break; + case AssetDataScenario.ERC20FiveDecimals: + makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]); + break; + case AssetDataScenario.ERC721: + makerAssetData = assetDataUtils.encodeERC721AssetData( + this._erc721Token.address, + erc721MakerAssetIds[0], + ); + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); + } + + switch (orderScenario.takerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + takerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress); + break; + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + takerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[1]); + break; + case AssetDataScenario.ERC20FiveDecimals: + takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]); + break; + case AssetDataScenario.ERC721: + takerAssetData = assetDataUtils.encodeERC721AssetData( + this._erc721Token.address, + erc721TakerAssetIds[0], + ); + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); + } + + switch (orderScenario.makerAssetAmountScenario) { + case OrderAssetAmountScenario.Large: + switch (orderScenario.makerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + makerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + makerAssetAmount = TEN_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + makerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); + } + break; + case OrderAssetAmountScenario.Small: + switch (orderScenario.makerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + makerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + makerAssetAmount = FIVE_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + makerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); + } + break; + case OrderAssetAmountScenario.Zero: + makerAssetAmount = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerAssetAmountScenario); + } + + switch (orderScenario.takerAssetAmountScenario) { + case OrderAssetAmountScenario.Large: + switch (orderScenario.takerAssetDataScenario) { + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + case AssetDataScenario.ZRXFeeToken: + takerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + takerAssetAmount = TEN_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + takerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); + } + break; + case OrderAssetAmountScenario.Small: + switch (orderScenario.takerAssetDataScenario) { + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + case AssetDataScenario.ZRXFeeToken: + takerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + takerAssetAmount = FIVE_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + takerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); + } + break; + case OrderAssetAmountScenario.Zero: + takerAssetAmount = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerAssetAmountScenario); + } + + switch (orderScenario.makerFeeScenario) { + case OrderAssetAmountScenario.Large: + makerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAssetAmountScenario.Small: + makerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAssetAmountScenario.Zero: + makerFee = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerFeeScenario); + } + + switch (orderScenario.takerFeeScenario) { + case OrderAssetAmountScenario.Large: + takerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAssetAmountScenario.Small: + takerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAssetAmountScenario.Zero: + takerFee = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerFeeScenario); + } + + switch (orderScenario.expirationTimeSecondsScenario) { + case ExpirationTimeSecondsScenario.InFuture: + expirationTimeSeconds = new BigNumber(2524604400); // Close to infinite + break; + case ExpirationTimeSecondsScenario.InPast: + expirationTimeSeconds = new BigNumber(0); // Jan 1, 1970 + break; + default: + throw errorUtils.spawnSwitchErr( + 'ExpirationTimeSecondsScenario', + orderScenario.expirationTimeSecondsScenario, + ); + } + + switch (orderScenario.takerScenario) { + case TakerScenario.CorrectlySpecified: + break; // noop since takerAddress is already specified + + case TakerScenario.IncorrectlySpecified: + const notTaker = this._userAddresses[3]; + takerAddress = notTaker; + break; + + case TakerScenario.Unspecified: + takerAddress = constants.NULL_ADDRESS; + break; + + default: + throw errorUtils.spawnSwitchErr('TakerScenario', orderScenario.takerScenario); + } + + const order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress, + takerAddress, + makerFee, + takerFee, + makerAssetAmount, + takerAssetAmount, + makerAssetData, + takerAssetData, + salt: generatePseudoRandomSalt(), + exchangeAddress: this._exchangeAddress, + feeRecipientAddress, + expirationTimeSeconds, + }; + + return order; + } +} diff --git a/packages/contracts/test/utils/order_utils.ts b/packages/contracts/test/utils/order_utils.ts new file mode 100644 index 000000000..019f6e74b --- /dev/null +++ b/packages/contracts/test/utils/order_utils.ts @@ -0,0 +1,58 @@ +import { OrderWithoutExchangeAddress, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { constants } from './constants'; +import { CancelOrder, MatchOrder } from './types'; + +export const orderUtils = { + getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const partialAmount = numerator + .mul(target) + .div(denominator) + .floor(); + return partialAmount; + }, + createFill: (signedOrder: SignedOrder, takerAssetFillAmount?: BigNumber) => { + const fill = { + order: orderUtils.getOrderWithoutExchangeAddress(signedOrder), + takerAssetFillAmount: takerAssetFillAmount || signedOrder.takerAssetAmount, + signature: signedOrder.signature, + }; + return fill; + }, + createCancel(signedOrder: SignedOrder, takerAssetCancelAmount?: BigNumber): CancelOrder { + const cancel = { + order: orderUtils.getOrderWithoutExchangeAddress(signedOrder), + takerAssetCancelAmount: takerAssetCancelAmount || signedOrder.takerAssetAmount, + }; + return cancel; + }, + getOrderWithoutExchangeAddress(signedOrder: SignedOrder): OrderWithoutExchangeAddress { + const orderStruct = { + senderAddress: signedOrder.senderAddress, + makerAddress: signedOrder.makerAddress, + takerAddress: signedOrder.takerAddress, + feeRecipientAddress: signedOrder.feeRecipientAddress, + makerAssetAmount: signedOrder.makerAssetAmount, + takerAssetAmount: signedOrder.takerAssetAmount, + makerFee: signedOrder.makerFee, + takerFee: signedOrder.takerFee, + expirationTimeSeconds: signedOrder.expirationTimeSeconds, + salt: signedOrder.salt, + makerAssetData: signedOrder.makerAssetData, + takerAssetData: signedOrder.takerAssetData, + }; + return orderStruct; + }, + createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder): MatchOrder { + const fill = { + left: orderUtils.getOrderWithoutExchangeAddress(signedOrderLeft), + right: orderUtils.getOrderWithoutExchangeAddress(signedOrderRight), + leftSignature: signedOrderLeft.signature, + rightSignature: signedOrderRight.signature, + }; + fill.right.makerAssetData = constants.NULL_BYTES; + fill.right.takerAssetData = constants.NULL_BYTES; + return fill; + }, +}; diff --git a/packages/contracts/test/utils/profiler.ts b/packages/contracts/test/utils/profiler.ts new file mode 100644 index 000000000..85ee24f22 --- /dev/null +++ b/packages/contracts/test/utils/profiler.ts @@ -0,0 +1,27 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { ProfilerSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let profilerSubprovider: ProfilerSubprovider; + +export const profiler = { + start(): void { + profiler.getProfilerSubproviderSingleton().start(); + }, + stop(): void { + profiler.getProfilerSubproviderSingleton().stop(); + }, + getProfilerSubproviderSingleton(): ProfilerSubprovider { + if (_.isUndefined(profilerSubprovider)) { + profilerSubprovider = profiler._getProfilerSubprovider(); + } + return profilerSubprovider; + }, + _getProfilerSubprovider(): ProfilerSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new ProfilerSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/revert_trace.ts b/packages/contracts/test/utils/revert_trace.ts new file mode 100644 index 000000000..0bf8384bc --- /dev/null +++ b/packages/contracts/test/utils/revert_trace.ts @@ -0,0 +1,21 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { RevertTraceSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let revertTraceSubprovider: RevertTraceSubprovider; + +export const revertTrace = { + getRevertTraceSubproviderSingleton(): RevertTraceSubprovider { + if (_.isUndefined(revertTraceSubprovider)) { + revertTraceSubprovider = revertTrace._getRevertTraceSubprovider(); + } + return revertTraceSubprovider; + }, + _getRevertTraceSubprovider(): RevertTraceSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new RevertTraceSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/signing_utils.ts b/packages/contracts/test/utils/signing_utils.ts new file mode 100644 index 000000000..9c711c72c --- /dev/null +++ b/packages/contracts/test/utils/signing_utils.ts @@ -0,0 +1,29 @@ +import { SignatureType } from '@0xproject/types'; +import * as ethUtil from 'ethereumjs-util'; + +export const signingUtils = { + signMessage(message: Buffer, privateKey: Buffer, signatureType: SignatureType): Buffer { + if (signatureType === SignatureType.EthSign) { + const prefixedMessage = ethUtil.hashPersonalMessage(message); + const ecSignature = ethUtil.ecsign(prefixedMessage, privateKey); + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(signatureType), + ]); + return signature; + } else if (signatureType === SignatureType.EIP712) { + const ecSignature = ethUtil.ecsign(message, privateKey); + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(signatureType), + ]); + return signature; + } else { + throw new Error(`${signatureType} is not a valid signature type`); + } + }, +}; diff --git a/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..598ee6d29 --- /dev/null +++ b/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,19 @@ +import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { AssetWrapper } from './asset_wrapper'; + +export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private readonly _assetWrapper: AssetWrapper; + constructor(assetWrapper: AssetWrapper) { + this._assetWrapper = assetWrapper; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData); + return proxyAllowance; + } +} diff --git a/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts b/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..ed69ecc63 --- /dev/null +++ b/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts @@ -0,0 +1,24 @@ +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { ExchangeWrapper } from './exchange_wrapper'; + +export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher { + private readonly _exchangeWrapper: ExchangeWrapper; + private readonly _zrxAssetData: string; + constructor(exchange: ExchangeWrapper, zrxAssetData: string) { + this._exchangeWrapper = exchange; + this._zrxAssetData = zrxAssetData; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash)); + return filledTakerAmount; + } + public async isOrderCancelledAsync(orderHash: string): Promise<boolean> { + const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash); + return isCancelled; + } + public getZRXAssetData(): string { + return this._zrxAssetData; + } +} diff --git a/packages/contracts/test/utils/test_with_reference.ts b/packages/contracts/test/utils/test_with_reference.ts new file mode 100644 index 000000000..599b1eed4 --- /dev/null +++ b/packages/contracts/test/utils/test_with_reference.ts @@ -0,0 +1,119 @@ +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { chaiSetup } from './chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +export async function testWithReferenceFuncAsync<P0, R>( + referenceFunc: (p0: P0) => Promise<R>, + testFunc: (p0: P0) => Promise<R>, + values: [P0], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, R>( + referenceFunc: (p0: P0, p1: P1) => Promise<R>, + testFunc: (p0: P0, p1: P1) => Promise<R>, + values: [P0, P1], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + values: [P0, P1, P2], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, P3, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + values: [P0, P1, P2, P3], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + values: [P0, P1, P2, P3, P4], +): Promise<void>; + +/** + * Tests the behavior of a test function by comparing it to the expected + * behavior (defined by a reference function). + * + * First the reference function will be called to obtain an "expected result", + * or if the reference function throws/rejects, an "expected error". Next, the + * test function will be called to obtain an "actual result", or if the test + * function throws/rejects, an "actual error". The test passes if at least one + * of the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or throws/rejects with the "expected error". + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or throws/rejects with the "actual error". + * @param values an array of N values, where each value corresponds in-order to + * an argument to both the test function and the reference function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testWithReferenceFuncAsync( + referenceFuncAsync: (...args: any[]) => Promise<any>, + testFuncAsync: (...args: any[]) => Promise<any>, + values: any[], +): Promise<void> { + let expectedResult: any; + let expectedErr: string | undefined; + try { + expectedResult = await referenceFuncAsync(...values); + } catch (e) { + expectedErr = e.message; + } + let actualResult: any | undefined; + try { + actualResult = await testFuncAsync(...values); + if (!_.isUndefined(expectedErr)) { + throw new Error( + `Expected error containing ${expectedErr} but got no error\n\tTest case: ${_getTestCaseString( + referenceFuncAsync, + values, + )}`, + ); + } + } catch (e) { + if (_.isUndefined(expectedErr)) { + throw new Error(`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`); + } else { + expect(e.message).to.contain( + expectedErr, + `${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } + } + if (!_.isUndefined(actualResult) && !_.isUndefined(expectedResult)) { + expect(actualResult).to.deep.equal( + expectedResult, + `Test case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } +} + +function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string { + const paramNames = _getParameterNames(referenceFuncAsync); + return JSON.stringify(_.zipObject(paramNames, values)); +} + +// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically +function _getParameterNames(func: (...args: any[]) => any): string[] { + return _.toString(func) + .replace(/[/][/].*$/gm, '') // strip single-line comments + .replace(/\s+/g, '') // strip white space + .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments + .split('){', 1)[0] + .replace(/^[^(]*[(]/, '') // extract the parameters + .replace(/=[^,]+/g, '') // strip any ES6 defaults + .split(',') + .filter(Boolean); // split & filter [""] +} diff --git a/packages/contracts/test/utils/token_registry_wrapper.ts b/packages/contracts/test/utils/token_registry_wrapper.ts new file mode 100644 index 000000000..f1c40e8ff --- /dev/null +++ b/packages/contracts/test/utils/token_registry_wrapper.ts @@ -0,0 +1,66 @@ +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; + +import { TokenRegistryContract } from '../../generated_contract_wrappers/token_registry'; + +import { Token } from './types'; + +import { constants } from './constants'; + +export class TokenRegWrapper { + private readonly _tokenReg: TokenRegistryContract; + private readonly _web3Wrapper: Web3Wrapper; + constructor(tokenRegContract: TokenRegistryContract, provider: Provider) { + this._tokenReg = tokenRegContract; + this._web3Wrapper = new Web3Wrapper(provider); + } + public async addTokenAsync(token: Token, from: string): Promise<string> { + const txHash = await this._tokenReg.addToken.sendTransactionAsync( + token.address as string, + token.name, + token.symbol, + token.decimals, + token.ipfsHash, + token.swarmHash, + { from }, + ); + await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + return txHash; + } + public async getTokenMetaDataAsync(tokenAddress: string): Promise<Token> { + const data = await this._tokenReg.getTokenMetaData.callAsync(tokenAddress); + const token: Token = { + address: data[0], + name: data[1], + symbol: data[2], + decimals: data[3], + ipfsHash: data[4], + swarmHash: data[5], + }; + return token; + } + public async getTokenByNameAsync(tokenName: string): Promise<Token> { + const data = await this._tokenReg.getTokenByName.callAsync(tokenName); + const token: Token = { + address: data[0], + name: data[1], + symbol: data[2], + decimals: data[3], + ipfsHash: data[4], + swarmHash: data[5], + }; + return token; + } + public async getTokenBySymbolAsync(tokenSymbol: string): Promise<Token> { + const data = await this._tokenReg.getTokenBySymbol.callAsync(tokenSymbol); + const token: Token = { + address: data[0], + name: data[1], + symbol: data[2], + decimals: data[3], + ipfsHash: data[4], + swarmHash: data[5], + }; + return token; + } +} diff --git a/packages/contracts/test/utils/transaction_factory.ts b/packages/contracts/test/utils/transaction_factory.ts new file mode 100644 index 000000000..281c1e30d --- /dev/null +++ b/packages/contracts/test/utils/transaction_factory.ts @@ -0,0 +1,47 @@ +import { EIP712Schema, EIP712Types, EIP712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { SignatureType } from '@0xproject/types'; +import * as ethUtil from 'ethereumjs-util'; + +import { signingUtils } from './signing_utils'; +import { SignedTransaction } from './types'; + +const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = { + name: 'ZeroExTransaction', + parameters: [ + { name: 'salt', type: EIP712Types.Uint256 }, + { name: 'signerAddress', type: EIP712Types.Address }, + { name: 'data', type: EIP712Types.Bytes }, + ], +}; + +export class TransactionFactory { + private readonly _signerBuff: Buffer; + private readonly _exchangeAddress: string; + private readonly _privateKey: Buffer; + constructor(privateKey: Buffer, exchangeAddress: string) { + this._privateKey = privateKey; + this._exchangeAddress = exchangeAddress; + this._signerBuff = ethUtil.privateToAddress(this._privateKey); + } + public newSignedTransaction(data: string, signatureType: SignatureType = SignatureType.EthSign): SignedTransaction { + const salt = generatePseudoRandomSalt(); + const signerAddress = `0x${this._signerBuff.toString('hex')}`; + const executeTransactionData = { + salt, + signerAddress, + data, + }; + const executeTransactionHashBuff = EIP712Utils.structHash( + EIP712_ZEROEX_TRANSACTION_SCHEMA, + executeTransactionData, + ); + const txHash = EIP712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress); + const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType); + const signedTx = { + exchangeAddress: this._exchangeAddress, + signature: `0x${signature.toString('hex')}`, + ...executeTransactionData, + }; + return signedTx; + } +} diff --git a/packages/contracts/test/utils/type_encoding_utils.ts b/packages/contracts/test/utils/type_encoding_utils.ts new file mode 100644 index 000000000..75307b9bd --- /dev/null +++ b/packages/contracts/test/utils/type_encoding_utils.ts @@ -0,0 +1,21 @@ +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import ethUtil = require('ethereumjs-util'); + +import { constants } from './constants'; + +export const typeEncodingUtils = { + encodeUint256(value: BigNumber): Buffer { + const base = 10; + const formattedValue = new BN(value.toString(base)); + const encodedValue = ethUtil.toBuffer(formattedValue); + // tslint:disable-next-line:custom-no-magic-numbers + const paddedValue = ethUtil.setLengthLeft(encodedValue, constants.WORD_LENGTH); + return paddedValue; + }, + decodeUint256(encodedValue: Buffer): BigNumber { + const formattedValue = ethUtil.bufferToHex(encodedValue); + const value = new BigNumber(formattedValue, constants.BASE_16); + return value; + }, +}; diff --git a/packages/contracts/test/utils/types.ts b/packages/contracts/test/utils/types.ts new file mode 100644 index 000000000..67313b647 --- /dev/null +++ b/packages/contracts/test/utils/types.ts @@ -0,0 +1,237 @@ +import { OrderWithoutExchangeAddress } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { AbiDefinition } from 'ethereum-types'; + +export interface ERC20BalancesByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: BigNumber; + }; +} + +export interface ERC721TokenIdsByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: BigNumber[]; + }; +} + +export interface SubmissionContractEventArgs { + transactionId: BigNumber; +} + +export interface BatchFillOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + takerAssetFillAmounts: BigNumber[]; +} + +export interface MarketSellOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + takerAssetFillAmount: BigNumber; +} + +export interface MarketBuyOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + makerAssetFillAmount: BigNumber; +} + +export interface BatchCancelOrders { + orders: OrderWithoutExchangeAddress[]; +} + +export interface CancelOrdersBefore { + salt: BigNumber; +} + +export interface TransactionDataParams { + name: string; + abi: AbiDefinition[]; + args: any[]; +} + +export interface MultiSigConfig { + owners: string[]; + confirmationsRequired: number; + secondsRequired: number; +} + +export interface MultiSigConfigByNetwork { + [networkName: string]: MultiSigConfig; +} + +export interface Token { + address?: string; + name: string; + symbol: string; + decimals: number; + ipfsHash: string; + swarmHash: string; +} + +export enum OrderStatus { + INVALID, + INVALID_MAKER_ASSET_AMOUNT, + INVALID_TAKER_ASSET_AMOUNT, + FILLABLE, + EXPIRED, + FULLY_FILLED, + CANCELLED, +} + +export enum ContractName { + TokenRegistry = 'TokenRegistry', + MultiSigWalletWithTimeLock = 'MultiSigWalletWithTimeLock', + Exchange = 'Exchange', + ZRXToken = 'ZRXToken', + DummyERC20Token = 'DummyERC20Token', + EtherToken = 'WETH9', + AssetProxyOwner = 'AssetProxyOwner', + AccountLevels = 'AccountLevels', + EtherDelta = 'EtherDelta', + Arbitrage = 'Arbitrage', + TestAssetDataDecoders = 'TestAssetDataDecoders', + TestAssetProxyDispatcher = 'TestAssetProxyDispatcher', + TestLibs = 'TestLibs', + TestSignatureValidator = 'TestSignatureValidator', + ERC20Proxy = 'ERC20Proxy', + ERC721Proxy = 'ERC721Proxy', + DummyERC721Receiver = 'DummyERC721Receiver', + DummyERC721Token = 'DummyERC721Token', + TestLibBytes = 'TestLibBytes', + TestWallet = 'TestWallet', + Authorizable = 'Authorizable', + Whitelist = 'Whitelist', + Forwarder = 'Forwarder', +} + +export interface SignedTransaction { + exchangeAddress: string; + salt: BigNumber; + signerAddress: string; + data: string; + signature: string; +} + +export interface TransferAmountsByMatchOrders { + // Left Maker + amountBoughtByLeftMaker: BigNumber; + amountSoldByLeftMaker: BigNumber; + amountReceivedByLeftMaker: BigNumber; + feePaidByLeftMaker: BigNumber; + // Right Maker + amountBoughtByRightMaker: BigNumber; + amountSoldByRightMaker: BigNumber; + amountReceivedByRightMaker: BigNumber; + feePaidByRightMaker: BigNumber; + // Taker + amountReceivedByTaker: BigNumber; + feePaidByTakerLeft: BigNumber; + feePaidByTakerRight: BigNumber; + totalFeePaidByTaker: BigNumber; + // Fee Recipients + feeReceivedLeft: BigNumber; + feeReceivedRight: BigNumber; +} + +export interface OrderInfo { + orderStatus: number; + orderHash: string; + orderTakerAssetFilledAmount: BigNumber; +} + +export interface CancelOrder { + order: OrderWithoutExchangeAddress; + takerAssetCancelAmount: BigNumber; +} + +export interface MatchOrder { + left: OrderWithoutExchangeAddress; + right: OrderWithoutExchangeAddress; + leftSignature: string; + rightSignature: string; +} + +// Combinatorial testing types + +export enum FeeRecipientAddressScenario { + BurnAddress = 'BURN_ADDRESS', + EthUserAddress = 'ETH_USER_ADDRESS', +} + +export enum OrderAssetAmountScenario { + Zero = 'ZERO', + Large = 'LARGE', + Small = 'SMALL', +} + +export enum TakerScenario { + CorrectlySpecified = 'CORRECTLY_SPECIFIED', + IncorrectlySpecified = 'INCORRECTLY_SPECIFIED', + Unspecified = 'UNSPECIFIED', +} + +export enum ExpirationTimeSecondsScenario { + InPast = 'IN_PAST', + InFuture = 'IN_FUTURE', +} + +export enum AssetDataScenario { + ERC721 = 'ERC721', + ZRXFeeToken = 'ZRX_FEE_TOKEN', + ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS', + ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS', +} + +export enum TakerAssetFillAmountScenario { + Zero = 'ZERO', + GreaterThanRemainingFillableTakerAssetAmount = 'GREATER_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', + LessThanRemainingFillableTakerAssetAmount = 'LESS_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', + ExactlyRemainingFillableTakerAssetAmount = 'EXACTLY_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', +} + +export interface OrderScenario { + takerScenario: TakerScenario; + feeRecipientScenario: FeeRecipientAddressScenario; + makerAssetAmountScenario: OrderAssetAmountScenario; + takerAssetAmountScenario: OrderAssetAmountScenario; + makerFeeScenario: OrderAssetAmountScenario; + takerFeeScenario: OrderAssetAmountScenario; + expirationTimeSecondsScenario: ExpirationTimeSecondsScenario; + makerAssetDataScenario: AssetDataScenario; + takerAssetDataScenario: AssetDataScenario; +} + +export enum BalanceAmountScenario { + Exact = 'EXACT', + TooLow = 'TOO_LOW', + Higher = 'HIGHER', +} + +export enum AllowanceAmountScenario { + Exact = 'EXACT', + TooLow = 'TOO_LOW', + Higher = 'HIGHER', + Unlimited = 'UNLIMITED', +} + +export interface TraderStateScenario { + traderAssetBalance: BalanceAmountScenario; + traderAssetAllowance: AllowanceAmountScenario; + zrxFeeBalance: BalanceAmountScenario; + zrxFeeAllowance: AllowanceAmountScenario; +} + +export interface FillScenario { + orderScenario: OrderScenario; + takerAssetFillAmountScenario: TakerAssetFillAmountScenario; + makerStateScenario: TraderStateScenario; + takerStateScenario: TraderStateScenario; +} + +export interface FillResults { + makerAssetFilledAmount: BigNumber; + takerAssetFilledAmount: BigNumber; + makerFeePaid: BigNumber; + takerFeePaid: BigNumber; +} diff --git a/packages/contracts/test/utils/web3_wrapper.ts b/packages/contracts/test/utils/web3_wrapper.ts index 35a8095a6..acb3103b7 100644 --- a/packages/contracts/test/utils/web3_wrapper.ts +++ b/packages/contracts/test/utils/web3_wrapper.ts @@ -1,6 +1,84 @@ -import { web3Factory } from '@0xproject/dev-utils'; +import { devConstants, env, EnvVars, web3Factory } from '@0xproject/dev-utils'; +import { prependSubprovider } from '@0xproject/subproviders'; +import { logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; -const web3ProviderConfig = { shouldUseInProcessGanache: true }; -export const web3 = web3Factory.create(web3ProviderConfig); -export const web3Wrapper = new Web3Wrapper(web3.currentProvider); +import { coverage } from './coverage'; +import { profiler } from './profiler'; +import { revertTrace } from './revert_trace'; + +enum ProviderType { + Ganache = 'ganache', + Geth = 'geth', +} + +let testProvider: ProviderType; +switch (process.env.TEST_PROVIDER) { + case undefined: + testProvider = ProviderType.Ganache; + break; + case 'ganache': + testProvider = ProviderType.Ganache; + break; + case 'geth': + testProvider = ProviderType.Geth; + break; + default: + throw new Error(`Unknown TEST_PROVIDER: ${process.env.TEST_PROVIDER}`); +} + +const ganacheTxDefaults = { + from: devConstants.TESTRPC_FIRST_ADDRESS, + gas: devConstants.GAS_LIMIT, +}; +const gethTxDefaults = { + from: devConstants.TESTRPC_FIRST_ADDRESS, +}; +export const txDefaults = testProvider === ProviderType.Ganache ? ganacheTxDefaults : gethTxDefaults; + +const gethConfigs = { + shouldUseInProcessGanache: false, + rpcUrl: 'http://localhost:8501', + shouldUseFakeGasEstimate: false, +}; +const ganacheConfigs = { + shouldUseInProcessGanache: true, +}; +const providerConfigs = testProvider === ProviderType.Ganache ? ganacheConfigs : gethConfigs; + +export const provider = web3Factory.getRpcProvider(providerConfigs); +const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); +const isProfilerEnabled = env.parseBoolean(EnvVars.SolidityProfiler); +const isRevertTraceEnabled = env.parseBoolean(EnvVars.SolidityRevertTrace); +const enabledSubproviderCount = _.filter( + [isCoverageEnabled, isProfilerEnabled, isRevertTraceEnabled], + _.identity.bind(_), +).length; +if (enabledSubproviderCount > 1) { + throw new Error(`Only one of coverage, profiler, or revert trace subproviders can be enabled at a time`); +} +if (isCoverageEnabled) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + prependSubprovider(provider, coverageSubprovider); +} +if (isProfilerEnabled) { + if (testProvider === ProviderType.Ganache) { + logUtils.warn( + "Gas costs in Ganache traces are incorrect and we don't recommend using it for profiling. Please switch to Geth", + ); + process.exit(1); + } + const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + logUtils.log( + "By default profilerSubprovider is stopped so that you don't get noise from setup code. Don't forget to start it before the code you want to profile and stop it afterwards", + ); + profilerSubprovider.stop(); + prependSubprovider(provider, profilerSubprovider); +} +if (isRevertTraceEnabled) { + const revertTraceSubprovider = revertTrace.getRevertTraceSubproviderSingleton(); + prependSubprovider(provider, revertTraceSubprovider); +} + +export const web3Wrapper = new Web3Wrapper(provider); diff --git a/packages/contracts/test/zrx_token.ts b/packages/contracts/test/zrx_token.ts deleted file mode 100644 index a4cc3c2fc..000000000 --- a/packages/contracts/test/zrx_token.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { ZRXTokenContract } from '../src/contract_wrappers/generated/zrx_token'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('ZRXToken', () => { - let owner: string; - let spender: string; - let zeroEx: ZeroEx; - - let MAX_UINT: BigNumber; - - let zrx: ZRXTokenContract; - let zrxAddress: string; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = accounts[0]; - spender = accounts[1]; - zeroEx = new ZeroEx(web3.currentProvider, { - networkId: constants.TESTRPC_NETWORK_ID, - }); - const zrxInstance = await deployer.deployAsync(ContractName.ZRXToken); - zrx = new ZRXTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - zrxAddress = zrx.address; - MAX_UINT = zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('constants', () => { - it('should have 18 decimals', async () => { - const decimals = new BigNumber(await zrx.decimals.callAsync()); - const expectedDecimals = 18; - expect(decimals).to.be.bignumber.equal(expectedDecimals); - }); - - it('should have a total supply of 1 billion tokens', async () => { - const totalSupply = new BigNumber(await zrx.totalSupply.callAsync()); - const expectedTotalSupply = 1000000000; - expect(ZeroEx.toUnitAmount(totalSupply, 18)).to.be.bignumber.equal(expectedTotalSupply); - }); - - it('should be named 0x Protocol Token', async () => { - const name = await zrx.name.callAsync(); - const expectedName = '0x Protocol Token'; - expect(name).to.be.equal(expectedName); - }); - - it('should have the symbol ZRX', async () => { - const symbol = await zrx.symbol.callAsync(); - const expectedSymbol = 'ZRX'; - expect(symbol).to.be.equal(expectedSymbol); - }); - }); - - describe('constructor', () => { - it('should initialize owner balance to totalSupply', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const totalSupply = new BigNumber(await zrx.totalSupply.callAsync()); - expect(totalSupply).to.be.bignumber.equal(ownerBalance); - }); - }); - - describe('transfer', () => { - it('should transfer balance from sender to receiver', async () => { - const receiver = spender; - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = new BigNumber(1); - await zeroEx.token.transferAsync(zrxAddress, owner, receiver, amountToTransfer); - const finalOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const finalReceiverBalance = await zeroEx.token.getBalanceAsync(zrxAddress, receiver); - - const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); - const expectedFinalReceiverBalance = amountToTransfer; - expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); - expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); - }); - - it('should return true on a 0 value transfer', async () => { - const didReturnTrue = await zrx.transfer.callAsync(spender, new BigNumber(0), { - from: owner, - }); - expect(didReturnTrue).to.be.true(); - }); - }); - - describe('transferFrom', () => { - it('should return false if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_APPROVE_GAS, - }); - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.false(); - }); - - it('should return false if spender has insufficient allowance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = ownerBalance; - - const spenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - const spenderAllowanceIsInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; - expect(spenderAllowanceIsInsufficient).to.be.true(); - - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.false(); - }); - - it('should return true on a 0 value transfer', async () => { - const amountToTransfer = new BigNumber(0); - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.true(); - }); - - it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = MAX_UINT; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, initSpenderAllowance, { - gasLimit: constants.MAX_TOKEN_APPROVE_GAS, - }); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); - }); - - it('should transfer the correct balances if spender has sufficient allowance', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const initSpenderBalance = await zeroEx.token.getBalanceAsync(zrxAddress, spender); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const newSpenderBalance = await zeroEx.token.getBalanceAsync(zrxAddress, spender); - - expect(newOwnerBalance).to.be.bignumber.equal(0); - expect(newSpenderBalance).to.be.bignumber.equal(initSpenderBalance.plus(initOwnerBalance)); - }); - - it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, amountToTransfer); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - expect(newSpenderAllowance).to.be.bignumber.equal(0); - }); - }); -}); |