Files
protocol/contracts/exchange/test/dispatcher.ts
F. Eugene Aumson f11d8a5bd8 @0x/order-utils refactors for v3: orderParsingUtils, signatureUtils, orderHashUtils, RevertErrors, transactionHashUtils (#2321)
* move orderParsingUtils from order-utils to connect

* Remove many functions from signatureUtils

Removed from the exported object, that is.  All of them are used in
other existing code, so they were all moved to be as local to their
usage as possible.

* remove orderHashUtils.isValidOrderHash()

* Move all *RevertErrors from order-utils...

...into their respective @0x/contracts- packages.

* Refactor @0x/order-utils' orderHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed getOrderHash()
method in DevUtils.

* Move all *RevertErrors from @0x/utils...

...into their respective @0x/contracts- packages.

* rm transactionHashUtils.isValidTransactionHash()

* DevUtils.sol: Fail yarn test if too big to deploy

* Refactor @0x/order-utils transactionHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed
getTransactionHash() method in DevUtils.

* Consolidate `Removed export...` CHANGELOG entries

* Rm EthBalanceChecker from devutils wrapper exports

* Stop importing from '.' or '.../src'

* fix builds

* fix prettier; dangling promise

* increase max bundle size
2019-11-14 17:14:24 -05:00

394 lines
19 KiB
TypeScript

import {
artifacts as proxyArtifacts,
ERC20ProxyContract,
ERC20Wrapper,
ERC721ProxyContract,
ERC721Wrapper,
} from '@0x/contracts-asset-proxy';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import {
chaiSetup,
constants,
LogDecoder,
orderUtils,
provider,
txDefaults,
web3Wrapper,
} from '@0x/contracts-test-utils';
import { OwnableRevertErrors } from '@0x/contracts-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { AssetProxyId, RevertReason } from '@0x/types';
import { BigNumber, StringRevertError } from '@0x/utils';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import * as _ from 'lodash';
import ExchangeRevertErrors = require('../src/revert_errors');
import { artifacts } from './artifacts';
import { TestAssetProxyDispatcherAssetProxyRegisteredEventArgs, TestAssetProxyDispatcherContract } from './wrappers';
import { dependencyArtifacts } from './utils/dependency_artifacts';
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 erc20TokenA: DummyERC20TokenContract;
let erc20TokenB: DummyERC20TokenContract;
let erc20Proxy: ERC20ProxyContract;
let erc721Proxy: ERC721ProxyContract;
let assetProxyDispatcher: TestAssetProxyDispatcherContract;
let erc20Wrapper: ERC20Wrapper;
let erc721Wrapper: ERC721Wrapper;
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
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 = 2;
[erc20TokenA, erc20TokenB] = 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,
dependencyArtifacts,
);
await erc20Proxy.addAuthorizedAddress(assetProxyDispatcher.address).awaitTransactionSuccessAsync({
from: owner,
});
await erc721Proxy.addAuthorizedAddress(assetProxyDispatcher.address).awaitTransactionSuccessAsync({
from: owner,
});
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('registerAssetProxy', () => {
it('should record proxy upon registration', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC20).callAsync();
expect(proxyAddress).to.be.equal(erc20Proxy.address);
});
it('should be able to record multiple proxies', async () => {
// Record first proxy
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
let proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC20).callAsync();
expect(proxyAddress).to.be.equal(erc20Proxy.address);
// Record another proxy
await assetProxyDispatcher.registerAssetProxy(erc721Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC721).callAsync();
expect(proxyAddress).to.be.equal(erc721Proxy.address);
});
it('should revert if a proxy with the same id is already registered', async () => {
// Initial registration
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC20).callAsync();
expect(proxyAddress).to.be.equal(erc20Proxy.address);
// Deploy a new version of the ERC20 Transfer Proxy contract
const newErc20TransferProxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
proxyArtifacts.ERC20Proxy,
provider,
txDefaults,
dependencyArtifacts,
);
const expectedError = new ExchangeRevertErrors.AssetProxyExistsError(AssetProxyId.ERC20, proxyAddress);
const tx = assetProxyDispatcher.registerAssetProxy(newErc20TransferProxy.address).sendTransactionAsync({
from: owner,
});
return expect(tx).to.revertWith(expectedError);
});
it('should revert if requesting address is not owner', async () => {
const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner);
const tx = assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).sendTransactionAsync({
from: notOwner,
});
return expect(tx).to.revertWith(expectedError);
});
it('should revert if the proxy is not a contract address', async () => {
const errMessage = 'VM Exception while processing transaction: revert';
const tx = assetProxyDispatcher.registerAssetProxy(notOwner).sendTransactionAsync({
from: owner,
});
return expect(tx).to.be.rejectedWith(errMessage);
});
it('should log an event with correct arguments when an asset proxy is registered', async () => {
const logDecoder = new LogDecoder(web3Wrapper, artifacts);
const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).sendTransactionAsync({
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 assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC20).callAsync();
expect(proxyAddress).to.be.equal(erc20Proxy.address);
});
it('should return NULL address if requesting non-existent proxy', async () => {
const proxyAddress = await assetProxyDispatcher.getAssetProxy(AssetProxyId.ERC20).callAsync();
expect(proxyAddress).to.be.equal(constants.NULL_ADDRESS);
});
});
describe('dispatchTransferFrom', () => {
const orderHash = orderUtils.generatePseudoRandomOrderHash();
it('should dispatch transfer to registered proxy', async () => {
// Register ERC20 proxy
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
// Construct metadata for ERC20 proxy
const encodedAssetData = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
// Perform a transfer from makerAddress to takerAddress
const erc20Balances = await erc20Wrapper.getBalancesAsync();
const amount = new BigNumber(10);
await assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.awaitTransactionSuccessAsync({ from: owner });
// Verify transfer was successful
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][erc20TokenA.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][erc20TokenA.address].minus(amount),
);
expect(newBalances[takerAddress][erc20TokenA.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][erc20TokenA.address].plus(amount),
);
});
it('should not dispatch a transfer if amount == 0', async () => {
// Register ERC20 proxy
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
// Construct metadata for ERC20 proxy
const encodedAssetData = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
// Perform a transfer from makerAddress to takerAddress
const erc20Balances = await erc20Wrapper.getBalancesAsync();
const amount = constants.ZERO_AMOUNT;
const txReceipt = await assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.awaitTransactionSuccessAsync({ from: owner });
expect(txReceipt.logs.length).to.be.equal(0);
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances).to.deep.equal(erc20Balances);
});
it('should revert if dispatching to unregistered proxy', async () => {
// Construct metadata for ERC20 proxy
const encodedAssetData = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
// Perform a transfer from makerAddress to takerAddress
const amount = new BigNumber(10);
const expectedError = new ExchangeRevertErrors.AssetProxyDispatchError(
ExchangeRevertErrors.AssetProxyDispatchErrorCode.UnknownAssetProxy,
orderHash,
encodedAssetData,
);
const tx = assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.sendTransactionAsync({ from: owner });
return expect(tx).to.revertWith(expectedError);
});
it('should revert with the correct error when assetData length < 4 bytes', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const encodedAssetData = (await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync()).slice(0, 8);
const amount = new BigNumber(1);
const expectedError = new ExchangeRevertErrors.AssetProxyDispatchError(
ExchangeRevertErrors.AssetProxyDispatchErrorCode.InvalidAssetDataLength,
orderHash,
encodedAssetData,
);
const tx = assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.sendTransactionAsync({ from: owner });
return expect(tx).to.revertWith(expectedError);
});
it('should revert if assetData is not padded to 32 bytes (excluding the id)', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
// Shave off the last byte
const encodedAssetData = (await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync()).slice(
0,
72,
);
const amount = new BigNumber(1);
const expectedError = new ExchangeRevertErrors.AssetProxyDispatchError(
ExchangeRevertErrors.AssetProxyDispatchErrorCode.InvalidAssetDataLength,
orderHash,
encodedAssetData,
);
const tx = assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.sendTransactionAsync({ from: owner });
return expect(tx).to.revertWith(expectedError);
});
it('should revert with the reason provided by the AssetProxy when a transfer fails', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
await erc20TokenA.approve(erc20Proxy.address, constants.ZERO_AMOUNT).awaitTransactionSuccessAsync({
from: makerAddress,
});
const encodedAssetData = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
const amount = new BigNumber(1);
const nestedError = new StringRevertError(RevertReason.TransferFailed).encode();
const expectedError = new ExchangeRevertErrors.AssetProxyTransferError(
orderHash,
encodedAssetData,
nestedError,
);
const tx = assetProxyDispatcher
.dispatchTransferFrom(orderHash, encodedAssetData, makerAddress, takerAddress, amount)
.sendTransactionAsync({ from: owner });
return expect(tx).to.revertWith(expectedError);
});
});
describe('simulateDispatchTransferFromCalls', () => {
it('should revert with the information specific to the failed transfer', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const assetDataA = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
const assetDataB = await devUtils.encodeERC20AssetData(erc20TokenB.address).callAsync();
await erc20TokenB.approve(erc20Proxy.address, constants.ZERO_AMOUNT).awaitTransactionSuccessAsync({
from: makerAddress,
});
const transferIndexAsBytes32 = '0x0000000000000000000000000000000000000000000000000000000000000001';
const nestedError = new StringRevertError(RevertReason.TransferFailed).encode();
const expectedError = new ExchangeRevertErrors.AssetProxyTransferError(
transferIndexAsBytes32,
assetDataB,
nestedError,
);
const tx = assetProxyDispatcher
.simulateDispatchTransferFromCalls(
[assetDataA, assetDataB],
[makerAddress, makerAddress],
[takerAddress, takerAddress],
[new BigNumber(1), new BigNumber(1)],
)
.sendTransactionAsync();
return expect(tx).to.revertWith(expectedError);
});
it('should forward the revert reason from the underlying failed transfer', async () => {
const assetDataA = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
const assetDataB = await devUtils.encodeERC20AssetData(erc20TokenB.address).callAsync();
const transferIndexAsBytes32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
const expectedError = new ExchangeRevertErrors.AssetProxyDispatchError(
ExchangeRevertErrors.AssetProxyDispatchErrorCode.UnknownAssetProxy,
transferIndexAsBytes32,
assetDataA,
);
const tx = assetProxyDispatcher
.simulateDispatchTransferFromCalls(
[assetDataA, assetDataB],
[makerAddress, makerAddress],
[takerAddress, takerAddress],
[new BigNumber(1), new BigNumber(1)],
)
.sendTransactionAsync();
return expect(tx).to.revertWith(expectedError);
});
it('should revert with TRANSFERS_SUCCESSFUL if no transfers fail', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const assetDataA = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
const assetDataB = await devUtils.encodeERC20AssetData(erc20TokenB.address).callAsync();
const tx = assetProxyDispatcher
.simulateDispatchTransferFromCalls(
[assetDataA, assetDataB],
[makerAddress, makerAddress],
[takerAddress, takerAddress],
[new BigNumber(1), new BigNumber(1)],
)
.sendTransactionAsync();
return expect(tx).to.revertWith(RevertReason.TransfersSuccessful);
});
it('should not modify balances if all transfers are successful', async () => {
await assetProxyDispatcher.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync({
from: owner,
});
const assetDataA = await devUtils.encodeERC20AssetData(erc20TokenA.address).callAsync();
const assetDataB = await devUtils.encodeERC20AssetData(erc20TokenB.address).callAsync();
const balances = await erc20Wrapper.getBalancesAsync();
try {
await assetProxyDispatcher
.simulateDispatchTransferFromCalls(
[assetDataA, assetDataB],
[makerAddress, makerAddress],
[takerAddress, takerAddress],
[new BigNumber(1), new BigNumber(1)],
)
.awaitTransactionSuccessAsync();
} catch (err) {
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances).to.deep.equal(balances);
}
});
});
});
// tslint:enable:no-unnecessary-type-assertion