* update abi-gen with new method interfaces * wip: get all packages to build * wip: get all packages to build * Fix two contract wrapper calls * Export necessary types part of the contract wrapper public interfaces * Revive and fix wrapper_unit_tests * Remove duplicate type * Fix lib_exchange_rich_error_decoder tests * Fix remaining test failures in contracts-* packages * Prettier fixes * remove transactionHelper * lint and update changelogs * Fix prettier * Revert changes to reference docs * Add back changelog already published and add revert changelog entry * Add missing CHANGELOG entries * Add missing comma * Update mesh-rpc-client dep * Update Mesh RPC logic in @0x/orderbook to v6.0.1-beta * Align package versions
392 lines
19 KiB
TypeScript
392 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 { BlockchainLifecycle } from '@0x/dev-utils';
|
|
import { ExchangeRevertErrors } from '@0x/order-utils';
|
|
import { AssetProxyId, RevertReason } from '@0x/types';
|
|
import { BigNumber, OwnableRevertErrors, StringRevertError } from '@0x/utils';
|
|
import * as chai from 'chai';
|
|
import { LogWithDecodedArgs } from 'ethereum-types';
|
|
import * as _ from 'lodash';
|
|
|
|
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
|