import { blockchainTests, constants, expect, filterLogs, filterLogsToArguments, getRandomInteger, hexLeftPad, hexRandom, Numberish, randomAddress, TransactionHelper, } from '@0x/contracts-test-utils'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { DecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts, TestUniswapBridgeContract, TestUniswapBridgeEthToTokenTransferInputEventArgs as EthToTokenTransferInputArgs, TestUniswapBridgeEvents as ContractEvents, TestUniswapBridgeTokenApproveEventArgs as TokenApproveArgs, TestUniswapBridgeTokenToEthSwapInputEventArgs as TokenToEthSwapInputArgs, TestUniswapBridgeTokenToTokenTransferInputEventArgs as TokenToTokenTransferInputArgs, TestUniswapBridgeTokenTransferEventArgs as TokenTransferArgs, TestUniswapBridgeWethDepositEventArgs as WethDepositArgs, TestUniswapBridgeWethWithdrawEventArgs as WethWithdrawArgs, } from '../src'; blockchainTests.resets('UniswapBridge unit tests', env => { const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); let testContract: TestUniswapBridgeContract; let wethTokenAddress: string; before(async () => { testContract = await TestUniswapBridgeContract.deployFrom0xArtifactAsync( artifacts.TestUniswapBridge, env.provider, env.txDefaults, artifacts, ); wethTokenAddress = await testContract.wethToken.callAsync(); }); describe('isValidSignature()', () => { it('returns success bytes', async () => { const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381'; const result = await testContract.isValidSignature.callAsync(hexRandom(), hexRandom(_.random(0, 32))); expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE); }); }); describe('withdrawTo()', () => { interface WithdrawToOpts { fromTokenAddress: string; toTokenAddress: string; fromTokenBalance: Numberish; toAddress: string; amount: Numberish; exchangeRevertReason: string; exchangeFillAmount: Numberish; toTokenRevertReason: string; fromTokenRevertReason: string; } function createWithdrawToOpts(opts?: Partial): WithdrawToOpts { return { fromTokenAddress: constants.NULL_ADDRESS, toTokenAddress: constants.NULL_ADDRESS, fromTokenBalance: getRandomInteger(1, 1e18), toAddress: randomAddress(), amount: getRandomInteger(1, 1e18), exchangeRevertReason: '', exchangeFillAmount: getRandomInteger(1, 1e18), toTokenRevertReason: '', fromTokenRevertReason: '', ...opts, }; } interface WithdrawToResult { opts: WithdrawToOpts; result: string; logs: DecodedLogs; blockTime: number; } async function withdrawToAsync(opts?: Partial): Promise { const _opts = createWithdrawToOpts(opts); // Create the "from" token and exchange. [[_opts.fromTokenAddress]] = await txHelper.getResultAndReceiptAsync( testContract.createTokenAndExchange, _opts.fromTokenAddress, _opts.exchangeRevertReason, { value: new BigNumber(_opts.exchangeFillAmount) }, ); // Create the "to" token and exchange. [[_opts.toTokenAddress]] = await txHelper.getResultAndReceiptAsync( testContract.createTokenAndExchange, _opts.toTokenAddress, _opts.exchangeRevertReason, { value: new BigNumber(_opts.exchangeFillAmount) }, ); await testContract.setTokenRevertReason.awaitTransactionSuccessAsync( _opts.toTokenAddress, _opts.toTokenRevertReason, ); await testContract.setTokenRevertReason.awaitTransactionSuccessAsync( _opts.fromTokenAddress, _opts.fromTokenRevertReason, ); // Set the token balance for the token we're converting from. await testContract.setTokenBalance.awaitTransactionSuccessAsync(_opts.fromTokenAddress, { value: new BigNumber(_opts.fromTokenBalance), }); // Call withdrawTo(). const [result, receipt] = await txHelper.getResultAndReceiptAsync( testContract.withdrawTo, // The "to" token address. _opts.toTokenAddress, // The "from" address. randomAddress(), // The "to" address. _opts.toAddress, // The amount to transfer to "to" new BigNumber(_opts.amount), // ABI-encoded "from" token address. hexLeftPad(_opts.fromTokenAddress), ); return { opts: _opts, result, logs: (receipt.logs as any) as DecodedLogs, blockTime: await env.web3Wrapper.getBlockTimestampAsync(receipt.blockNumber), }; } async function getExchangeForTokenAsync(tokenAddress: string): Promise { return testContract.getExchange.callAsync(tokenAddress); } it('returns magic bytes on success', async () => { const { result } = await withdrawToAsync(); expect(result).to.eq(AssetProxyId.ERC20Bridge); }); it('just transfers tokens to `to` if the same tokens are in play', async () => { const [[tokenAddress]] = await txHelper.getResultAndReceiptAsync( testContract.createTokenAndExchange, constants.NULL_ADDRESS, '', ); const { opts, result, logs } = await withdrawToAsync({ fromTokenAddress: tokenAddress, toTokenAddress: tokenAddress, }); expect(result).to.eq(AssetProxyId.ERC20Bridge); const transfers = filterLogsToArguments(logs, ContractEvents.TokenTransfer); expect(transfers.length).to.eq(1); expect(transfers[0].token).to.eq(tokenAddress); expect(transfers[0].from).to.eq(testContract.address); expect(transfers[0].to).to.eq(opts.toAddress); expect(transfers[0].amount).to.bignumber.eq(opts.amount); }); describe('token -> token', () => { it('calls `IUniswapExchange.tokenToTokenTransferInput()', async () => { const { opts, logs, blockTime } = await withdrawToAsync(); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); const calls = filterLogsToArguments( logs, ContractEvents.TokenToTokenTransferInput, ); expect(calls.length).to.eq(1); expect(calls[0].exchange).to.eq(exchangeAddress); expect(calls[0].tokensSold).to.bignumber.eq(opts.fromTokenBalance); expect(calls[0].minTokensBought).to.bignumber.eq(opts.amount); expect(calls[0].minEthBought).to.bignumber.eq(0); expect(calls[0].deadline).to.bignumber.eq(blockTime); expect(calls[0].recipient).to.eq(opts.toAddress); expect(calls[0].toTokenAddress).to.eq(opts.toTokenAddress); }); it('sets allowance for "from" token', async () => { const { opts, logs } = await withdrawToAsync(); const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); expect(approvals.length).to.eq(1); expect(approvals[0].spender).to.eq(exchangeAddress); expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); }); it('sets allowance for "from" token on subsequent calls', async () => { const { opts } = await withdrawToAsync(); const { logs } = await withdrawToAsync(opts); const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); expect(approvals.length).to.eq(1); expect(approvals[0].spender).to.eq(exchangeAddress); expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); }); it('fails if "from" token does not exist', async () => { const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( randomAddress(), randomAddress(), randomAddress(), getRandomInteger(1, 1e18), hexLeftPad(randomAddress()), ); return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); }); it('fails if the exchange fails', async () => { const revertReason = 'FOOBAR'; const tx = withdrawToAsync({ exchangeRevertReason: revertReason, }); return expect(tx).to.revertWith(revertReason); }); }); describe('token -> ETH', () => { it('calls `IUniswapExchange.tokenToEthSwapInput()`, `WETH.deposit()`, then `transfer()`', async () => { const { opts, logs, blockTime } = await withdrawToAsync({ toTokenAddress: wethTokenAddress, }); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); let calls: any = filterLogs(logs, ContractEvents.TokenToEthSwapInput); expect(calls.length).to.eq(1); expect(calls[0].args.exchange).to.eq(exchangeAddress); expect(calls[0].args.tokensSold).to.bignumber.eq(opts.fromTokenBalance); expect(calls[0].args.minEthBought).to.bignumber.eq(opts.amount); expect(calls[0].args.deadline).to.bignumber.eq(blockTime); calls = filterLogs( logs.slice(calls[0].logIndex as number), ContractEvents.WethDeposit, ); expect(calls.length).to.eq(1); expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount); calls = filterLogs( logs.slice(calls[0].logIndex as number), ContractEvents.TokenTransfer, ); expect(calls.length).to.eq(1); expect(calls[0].args.token).to.eq(opts.toTokenAddress); expect(calls[0].args.from).to.eq(testContract.address); expect(calls[0].args.to).to.eq(opts.toAddress); expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount); }); it('sets allowance for "from" token', async () => { const { opts, logs } = await withdrawToAsync({ toTokenAddress: wethTokenAddress, }); const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); expect(transfers.length).to.eq(1); expect(transfers[0].spender).to.eq(exchangeAddress); expect(transfers[0].allowance).to.bignumber.eq(constants.MAX_UINT256); }); it('sets allowance for "from" token on subsequent calls', async () => { const { opts } = await withdrawToAsync({ toTokenAddress: wethTokenAddress, }); const { logs } = await withdrawToAsync(opts); const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); expect(approvals.length).to.eq(1); expect(approvals[0].spender).to.eq(exchangeAddress); expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); }); it('fails if "from" token does not exist', async () => { const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( randomAddress(), randomAddress(), randomAddress(), getRandomInteger(1, 1e18), hexLeftPad(wethTokenAddress), ); return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); }); it('fails if `WETH.deposit()` fails', async () => { const revertReason = 'FOOBAR'; const tx = withdrawToAsync({ toTokenAddress: wethTokenAddress, toTokenRevertReason: revertReason, }); return expect(tx).to.revertWith(revertReason); }); it('fails if the exchange fails', async () => { const revertReason = 'FOOBAR'; const tx = withdrawToAsync({ toTokenAddress: wethTokenAddress, exchangeRevertReason: revertReason, }); return expect(tx).to.revertWith(revertReason); }); }); describe('ETH -> token', () => { it('calls `WETH.withdraw()`, then `IUniswapExchange.ethToTokenTransferInput()`', async () => { const { opts, logs, blockTime } = await withdrawToAsync({ fromTokenAddress: wethTokenAddress, }); const exchangeAddress = await getExchangeForTokenAsync(opts.toTokenAddress); let calls: any = filterLogs(logs, ContractEvents.WethWithdraw); expect(calls.length).to.eq(1); expect(calls[0].args.amount).to.bignumber.eq(opts.fromTokenBalance); calls = filterLogs( logs.slice(calls[0].logIndex as number), ContractEvents.EthToTokenTransferInput, ); expect(calls.length).to.eq(1); expect(calls[0].args.exchange).to.eq(exchangeAddress); expect(calls[0].args.minTokensBought).to.bignumber.eq(opts.amount); expect(calls[0].args.deadline).to.bignumber.eq(blockTime); expect(calls[0].args.recipient).to.eq(opts.toAddress); }); it('does not set any allowance', async () => { const { logs } = await withdrawToAsync({ fromTokenAddress: wethTokenAddress, }); const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); expect(approvals).to.be.empty(''); }); it('fails if "to" token does not exist', async () => { const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( wethTokenAddress, randomAddress(), randomAddress(), getRandomInteger(1, 1e18), hexLeftPad(randomAddress()), ); return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN'); }); it('fails if the `WETH.withdraw()` fails', async () => { const revertReason = 'FOOBAR'; const tx = withdrawToAsync({ fromTokenAddress: wethTokenAddress, fromTokenRevertReason: revertReason, }); return expect(tx).to.revertWith(revertReason); }); it('fails if the exchange fails', async () => { const revertReason = 'FOOBAR'; const tx = withdrawToAsync({ fromTokenAddress: wethTokenAddress, exchangeRevertReason: revertReason, }); return expect(tx).to.revertWith(revertReason); }); }); }); });