protocol/contracts/asset-proxy/test/erc20bridge_proxy.ts
Xianny fcbcbac889
Remove assetDataUtils everywhere (#2373)
* remove assetDataUtils everywhere

* export IAssetDataContract from @0x/contract-wrappers to allow @0x/instant to decode asset data  synchronously

* export generic function `decodeAssetDataOrThrow` and add ERC20Bridge support

* export `hexUtils` from order-utils instead of contracts-test-utils
2019-12-04 13:08:08 -08:00

288 lines
11 KiB
TypeScript

import {
blockchainTests,
constants,
expect,
getRandomInteger,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { AuthorizableRevertErrors } from '@0x/contracts-utils';
import { AssetProxyId } from '@0x/types';
import { AbiEncoder, BigNumber, hexUtils, StringRevertError } from '@0x/utils';
import { DecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { ERC20BridgeProxyContract, TestERC20BridgeContract } from './wrappers';
blockchainTests.resets('ERC20BridgeProxy unit tests', env => {
const PROXY_ID = AssetProxyId.ERC20Bridge;
const BRIDGE_SUCCESS_RETURN_DATA = hexUtils.rightPad(PROXY_ID);
let owner: string;
let badCaller: string;
let assetProxy: ERC20BridgeProxyContract;
let bridgeContract: TestERC20BridgeContract;
let testTokenAddress: string;
before(async () => {
[owner, badCaller] = await env.getAccountAddressesAsync();
assetProxy = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync(
artifacts.ERC20BridgeProxy,
env.provider,
env.txDefaults,
artifacts,
);
bridgeContract = await TestERC20BridgeContract.deployFrom0xArtifactAsync(
artifacts.TestERC20Bridge,
env.provider,
env.txDefaults,
artifacts,
);
testTokenAddress = await bridgeContract.testToken().callAsync();
await assetProxy.addAuthorizedAddress(owner).awaitTransactionSuccessAsync();
});
interface AssetDataOpts {
tokenAddress: string;
bridgeAddress: string;
bridgeData: BridgeDataOpts;
}
interface BridgeDataOpts {
transferAmount: Numberish;
revertError?: string;
returnData: string;
}
function createAssetData(opts?: Partial<AssetDataOpts>): AssetDataOpts {
return _.merge(
{
tokenAddress: testTokenAddress,
bridgeAddress: bridgeContract.address,
bridgeData: createBridgeData(),
},
opts,
);
}
function createBridgeData(opts?: Partial<BridgeDataOpts>): BridgeDataOpts {
return _.merge(
{
transferAmount: constants.ZERO_AMOUNT,
returnData: BRIDGE_SUCCESS_RETURN_DATA,
},
opts,
);
}
function encodeAssetData(opts: AssetDataOpts): string {
const encoder = AbiEncoder.createMethod('ERC20BridgeProxy', [
{ name: 'tokenAddress', type: 'address' },
{ name: 'bridgeAddress', type: 'address' },
{ name: 'bridgeData', type: 'bytes' },
]);
return encoder.encode([opts.tokenAddress, opts.bridgeAddress, encodeBridgeData(opts.bridgeData)]);
}
function encodeBridgeData(opts: BridgeDataOpts): string {
const encoder = AbiEncoder.create([
{ name: 'transferAmount', type: 'int256' },
{ name: 'revertData', type: 'bytes' },
{ name: 'returnData', type: 'bytes' },
]);
const revertErrorBytes =
opts.revertError !== undefined ? new StringRevertError(opts.revertError).encode() : '0x';
return encoder.encode([new BigNumber(opts.transferAmount), revertErrorBytes, opts.returnData]);
}
async function setTestTokenBalanceAsync(_owner: string, balance: Numberish): Promise<void> {
await bridgeContract.setTestTokenBalance(_owner, new BigNumber(balance)).awaitTransactionSuccessAsync();
}
describe('transferFrom()', () => {
interface TransferFromOpts {
assetData: AssetDataOpts;
from: string;
to: string;
amount: Numberish;
}
function createTransferFromOpts(opts?: Partial<TransferFromOpts>): TransferFromOpts {
const transferAmount = _.get(opts, ['amount'], getRandomInteger(1, 100e18)) as BigNumber;
return _.merge(
{
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount,
}),
}),
from: randomAddress(),
to: randomAddress(),
amount: transferAmount,
},
opts,
);
}
async function transferFromAsync(opts?: Partial<TransferFromOpts>, caller?: string): Promise<DecodedLogs> {
const _opts = createTransferFromOpts(opts);
const { logs } = await assetProxy
.transferFrom(encodeAssetData(_opts.assetData), _opts.from, _opts.to, new BigNumber(_opts.amount))
.awaitTransactionSuccessAsync({ from: caller });
return (logs as any) as DecodedLogs;
}
it('succeeds if the bridge succeeds and balance increases by `amount`', async () => {
const tx = transferFromAsync();
return expect(tx).to.be.fulfilled('');
});
it('succeeds if balance increases more than `amount`', async () => {
const amount = getRandomInteger(1, 100e18);
const tx = transferFromAsync({
amount,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: amount.plus(1),
}),
}),
});
return expect(tx).to.be.fulfilled('');
});
it('passes the correct arguments to the bridge contract', async () => {
const opts = createTransferFromOpts();
const logs = await transferFromAsync(opts);
expect(logs.length).to.eq(1);
const args = logs[0].args;
expect(args.tokenAddress).to.eq(opts.assetData.tokenAddress);
expect(args.from).to.eq(opts.from);
expect(args.to).to.eq(opts.to);
expect(args.amount).to.bignumber.eq(opts.amount);
expect(args.bridgeData).to.eq(encodeBridgeData(opts.assetData.bridgeData));
});
it('fails if not called by an authorized address', async () => {
const tx = transferFromAsync({}, badCaller);
return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(badCaller));
});
it('fails if asset data is truncated', async () => {
const opts = createTransferFromOpts();
const truncatedAssetData = hexUtils.slice(encodeAssetData(opts.assetData), 0, -1);
const tx = assetProxy
.transferFrom(truncatedAssetData, opts.from, opts.to, new BigNumber(opts.amount))
.awaitTransactionSuccessAsync();
return expect(tx).to.be.rejected();
});
it('fails if bridge returns nothing', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: '0x',
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge returns true', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: hexUtils.leftPad('0x1'),
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge returns 0x1', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: hexUtils.rightPad('0x1'),
}),
}),
});
return expect(tx).to.revertWith('BRIDGE_FAILED');
});
it('fails if bridge is an EOA', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeAddress: randomAddress(),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge reverts', async () => {
const revertError = 'FOOBAR';
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
revertError,
}),
}),
});
return expect(tx).to.revertWith(revertError);
});
it('fails if balance of `to` increases by less than `amount`', async () => {
const amount = getRandomInteger(1, 100e18);
const tx = transferFromAsync({
amount,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: amount.minus(1),
}),
}),
});
return expect(tx).to.revertWith('BRIDGE_UNDERPAY');
});
it('fails if balance of `to` decreases', async () => {
const toAddress = randomAddress();
await setTestTokenBalanceAsync(toAddress, 1e18);
const tx = transferFromAsync({
to: toAddress,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: -1,
}),
}),
});
return expect(tx).to.revertWith('BRIDGE_UNDERPAY');
});
});
describe('balanceOf()', () => {
it('retrieves the balance of the encoded token', async () => {
const _owner = randomAddress();
const balance = getRandomInteger(1, 100e18);
await bridgeContract.setTestTokenBalance(_owner, balance).awaitTransactionSuccessAsync();
const assetData = createAssetData({
tokenAddress: testTokenAddress,
});
const actualBalance = await assetProxy.balanceOf(encodeAssetData(assetData), _owner).callAsync();
expect(actualBalance).to.bignumber.eq(balance);
});
});
describe('getProxyId()', () => {
it('returns the correct proxy ID', async () => {
const proxyId = await assetProxy.getProxyId().callAsync();
expect(proxyId).to.eq(PROXY_ID);
});
});
});