* 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
		
			
				
	
	
		
			288 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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);
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |