* implement UniswapV2 Bridge * More tests for UniswapV2Bridge * cleanup and remove ERC20BridgeSampler changes * enable multihop; address review comments * solidity 0.6.9 doesnt allow devdoc for public storage vars * codestyle improvements
217 lines
9.7 KiB
TypeScript
217 lines
9.7 KiB
TypeScript
import {
|
|
blockchainTests,
|
|
constants,
|
|
expect,
|
|
filterLogsToArguments,
|
|
getRandomInteger,
|
|
randomAddress,
|
|
} from '@0x/contracts-test-utils';
|
|
import { AssetProxyId } from '@0x/types';
|
|
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
|
|
import { DecodedLogs } from 'ethereum-types';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts } from './artifacts';
|
|
|
|
import {
|
|
TestUniswapV2BridgeContract,
|
|
TestUniswapV2BridgeEvents as ContractEvents,
|
|
TestUniswapV2BridgeSwapExactTokensForTokensInputEventArgs as SwapExactTokensForTokensArgs,
|
|
TestUniswapV2BridgeTokenApproveEventArgs as TokenApproveArgs,
|
|
TestUniswapV2BridgeTokenTransferEventArgs as TokenTransferArgs,
|
|
} from './wrappers';
|
|
|
|
blockchainTests.resets('UniswapV2 unit tests', env => {
|
|
const FROM_TOKEN_DECIMALS = 6;
|
|
const TO_TOKEN_DECIMALS = 18;
|
|
const FROM_TOKEN_BASE = new BigNumber(10).pow(FROM_TOKEN_DECIMALS);
|
|
const TO_TOKEN_BASE = new BigNumber(10).pow(TO_TOKEN_DECIMALS);
|
|
let testContract: TestUniswapV2BridgeContract;
|
|
|
|
before(async () => {
|
|
testContract = await TestUniswapV2BridgeContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestUniswapV2Bridge,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
);
|
|
});
|
|
|
|
describe('isValidSignature()', () => {
|
|
it('returns success bytes', async () => {
|
|
const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381';
|
|
const result = await testContract
|
|
.isValidSignature(hexUtils.random(), hexUtils.random(_.random(0, 32)))
|
|
.callAsync();
|
|
expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE);
|
|
});
|
|
});
|
|
|
|
describe('bridgeTransferFrom()', () => {
|
|
interface TransferFromOpts {
|
|
tokenAddressesPath: string[];
|
|
toAddress: string;
|
|
// Amount to pass into `bridgeTransferFrom()`
|
|
amount: BigNumber;
|
|
// Token balance of the bridge.
|
|
fromTokenBalance: BigNumber;
|
|
// Router reverts with this reason
|
|
routerRevertReason: string;
|
|
}
|
|
|
|
interface TransferFromResult {
|
|
opts: TransferFromOpts;
|
|
result: string;
|
|
logs: DecodedLogs;
|
|
blocktime: number;
|
|
}
|
|
|
|
function createTransferFromOpts(opts?: Partial<TransferFromOpts>): TransferFromOpts {
|
|
const amount = getRandomInteger(1, TO_TOKEN_BASE.times(100));
|
|
return {
|
|
tokenAddressesPath: Array(2).fill(constants.NULL_ADDRESS),
|
|
amount,
|
|
toAddress: randomAddress(),
|
|
fromTokenBalance: getRandomInteger(1, FROM_TOKEN_BASE.times(100)),
|
|
routerRevertReason: '',
|
|
...opts,
|
|
};
|
|
}
|
|
|
|
const bridgeDataEncoder = AbiEncoder.create('(address[])');
|
|
|
|
async function transferFromAsync(opts?: Partial<TransferFromOpts>): Promise<TransferFromResult> {
|
|
const _opts = createTransferFromOpts(opts);
|
|
|
|
for (let i = 0; i < _opts.tokenAddressesPath.length; i++) {
|
|
const createFromTokenFn = testContract.createToken(_opts.tokenAddressesPath[i]);
|
|
_opts.tokenAddressesPath[i] = await createFromTokenFn.callAsync();
|
|
await createFromTokenFn.awaitTransactionSuccessAsync();
|
|
}
|
|
|
|
// Set the token balance for the token we're converting from.
|
|
await testContract
|
|
.setTokenBalance(_opts.tokenAddressesPath[0], _opts.fromTokenBalance)
|
|
.awaitTransactionSuccessAsync();
|
|
|
|
// Set revert reason for the router.
|
|
await testContract.setRouterRevertReason(_opts.routerRevertReason).awaitTransactionSuccessAsync();
|
|
|
|
// Call bridgeTransferFrom().
|
|
const bridgeTransferFromFn = testContract.bridgeTransferFrom(
|
|
// Output token
|
|
_opts.tokenAddressesPath[_opts.tokenAddressesPath.length - 1],
|
|
// Random maker address.
|
|
randomAddress(),
|
|
// Recipient address.
|
|
_opts.toAddress,
|
|
// Transfer amount.
|
|
_opts.amount,
|
|
// ABI-encode the input token address as the bridge data. // FIXME
|
|
bridgeDataEncoder.encode([_opts.tokenAddressesPath]),
|
|
);
|
|
const result = await bridgeTransferFromFn.callAsync();
|
|
const receipt = await bridgeTransferFromFn.awaitTransactionSuccessAsync();
|
|
return {
|
|
opts: _opts,
|
|
result,
|
|
logs: (receipt.logs as any) as DecodedLogs,
|
|
blocktime: await env.web3Wrapper.getBlockTimestampAsync(receipt.blockNumber),
|
|
};
|
|
}
|
|
|
|
it('returns magic bytes on success', async () => {
|
|
const { result } = await transferFromAsync();
|
|
expect(result).to.eq(AssetProxyId.ERC20Bridge);
|
|
});
|
|
|
|
it('performs transfer when both tokens are the same', async () => {
|
|
const createTokenFn = testContract.createToken(constants.NULL_ADDRESS);
|
|
const tokenAddress = await createTokenFn.callAsync();
|
|
await createTokenFn.awaitTransactionSuccessAsync();
|
|
|
|
const { opts, result, logs } = await transferFromAsync({
|
|
tokenAddressesPath: [tokenAddress, tokenAddress],
|
|
});
|
|
expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id');
|
|
const transfers = filterLogsToArguments<TokenTransferArgs>(logs, ContractEvents.TokenTransfer);
|
|
|
|
expect(transfers.length).to.eq(1);
|
|
expect(transfers[0].token).to.eq(tokenAddress, 'input token address');
|
|
expect(transfers[0].from).to.eq(testContract.address);
|
|
expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address');
|
|
expect(transfers[0].amount).to.bignumber.eq(opts.amount, 'amount');
|
|
});
|
|
|
|
describe('token -> token', async () => {
|
|
it('calls UniswapV2Router01.swapExactTokensForTokens()', async () => {
|
|
const { opts, result, logs, blocktime } = await transferFromAsync();
|
|
expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id');
|
|
const transfers = filterLogsToArguments<SwapExactTokensForTokensArgs>(
|
|
logs,
|
|
ContractEvents.SwapExactTokensForTokensInput,
|
|
);
|
|
|
|
expect(transfers.length).to.eq(1);
|
|
expect(transfers[0].toTokenAddress).to.eq(
|
|
opts.tokenAddressesPath[opts.tokenAddressesPath.length - 1],
|
|
'output token address',
|
|
);
|
|
expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address');
|
|
expect(transfers[0].amountIn).to.bignumber.eq(opts.fromTokenBalance, 'input token amount');
|
|
expect(transfers[0].amountOutMin).to.bignumber.eq(opts.amount, 'output token amount');
|
|
expect(transfers[0].deadline).to.bignumber.eq(blocktime, 'deadline');
|
|
});
|
|
|
|
it('sets allowance for "from" token', async () => {
|
|
const { logs } = await transferFromAsync();
|
|
const approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
|
const routerAddress = await testContract.getRouterAddress().callAsync();
|
|
expect(approvals.length).to.eq(1);
|
|
expect(approvals[0].spender).to.eq(routerAddress);
|
|
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
|
});
|
|
|
|
it('sets allowance for "from" token on subsequent calls', async () => {
|
|
const { opts } = await transferFromAsync();
|
|
const { logs } = await transferFromAsync(opts);
|
|
const approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
|
const routerAddress = await testContract.getRouterAddress().callAsync();
|
|
expect(approvals.length).to.eq(1);
|
|
expect(approvals[0].spender).to.eq(routerAddress);
|
|
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
|
});
|
|
|
|
it('fails if the router fails', async () => {
|
|
const revertReason = 'FOOBAR';
|
|
const tx = transferFromAsync({
|
|
routerRevertReason: revertReason,
|
|
});
|
|
return expect(tx).to.eventually.be.rejectedWith(revertReason);
|
|
});
|
|
});
|
|
describe('token -> token -> token', async () => {
|
|
it('calls UniswapV2Router01.swapExactTokensForTokens()', async () => {
|
|
const { opts, result, logs, blocktime } = await transferFromAsync({
|
|
tokenAddressesPath: Array(3).fill(constants.NULL_ADDRESS),
|
|
});
|
|
expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id');
|
|
const transfers = filterLogsToArguments<SwapExactTokensForTokensArgs>(
|
|
logs,
|
|
ContractEvents.SwapExactTokensForTokensInput,
|
|
);
|
|
|
|
expect(transfers.length).to.eq(1);
|
|
expect(transfers[0].toTokenAddress).to.eq(
|
|
opts.tokenAddressesPath[opts.tokenAddressesPath.length - 1],
|
|
'output token address',
|
|
);
|
|
expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address');
|
|
expect(transfers[0].amountIn).to.bignumber.eq(opts.fromTokenBalance, 'input token amount');
|
|
expect(transfers[0].amountOutMin).to.bignumber.eq(opts.amount, 'output token amount');
|
|
expect(transfers[0].deadline).to.bignumber.eq(blocktime, 'deadline');
|
|
});
|
|
});
|
|
});
|
|
});
|