diff --git a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol index 68e140894e..f1a55d9faf 100644 --- a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol @@ -21,18 +21,18 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; -import "@0x/contracts-exchange/contracts/src/interfaces/IWallet.sol"; import "../interfaces/IUniswapExchangeFactory.sol"; import "../interfaces/IUniswapExchange.sol"; -import "./ERC20Bridge.sol"; +import "../interfaces/IWallet.sol"; +import "../interfaces/IERC20Bridge.sol"; // solhint-disable space-after-comma +// solhint-disable not-rely-on-time contract UniswapBridge is - ERC20Bridge, + IERC20Bridge, IWallet { - bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381; /* Mainnet addresses */ address constant public UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; @@ -45,8 +45,15 @@ contract UniswapBridge is IEtherToken weth; } - /// @dev Whether we've granted an allowance to a spender for a token. - mapping (address => mapping (address => bool)) private _hasAllowance; + // solhint-disable no-empty-blocks + /// @dev Payable fallback to receive ETH from uniswap. + function () + external + payable + {} + + /// @dev Whether we've granted an allowance to the exchange for a token. + mapping (address => bool) private _hasAllowance; /// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of /// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress` @@ -83,7 +90,7 @@ contract UniswapBridge is toTokenAddress ); // Grant an allowance to the exchange. - _grantAllowanceForTokens(address(state.exchange), [fromTokenAddress, toTokenAddress]); + _grantExchangeAllowance(state.exchange); // Get our balance of `fromTokenAddress` token. state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); // Get the weth contract. @@ -176,27 +183,16 @@ contract UniswapBridge is return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS); } - /// @dev Grants an unlimited allowance to `spender` for the tokens passed, - /// if they're not WETH and we haven't already granted `spender` an - /// allowance. - /// @param spender The spender being granted an aloowance. - /// @param tokenAddresses Array of token addresses. - function _grantAllowanceForTokens( - address spender, - address[2] memory tokenAddresses - ) + /// @dev Grants an unlimited allowance to the exchange for its token + /// on behalf of this contract, if we haven't already done so. + /// @param exchange The Uniswap token exchange. + function _grantExchangeAllowance(IUniswapExchange exchange) private { - address wethAddress = address(_getWethContract()); - mapping (address => bool) storage doesSpenderHaveAllowance = _hasAllowance[spender]; - for (uint256 i = 0; i < tokenAddresses.length; ++i) { - address tokenAddress = tokenAddresses[i]; - if (tokenAddress != wethAddress) { - if (!doesSpenderHaveAllowance[tokenAddress]) { - IERC20Token(tokenAddress).approve(spender, uint256(-1)); - doesSpenderHaveAllowance[tokenAddress] = true; - } - } + address tokenAddress = exchange.toTokenAddress(); + if (!_hasAllowance[tokenAddress]) { + IERC20Token(tokenAddress).approve(address(exchange), uint256(-1)); + _hasAllowance[tokenAddress] = true; } } diff --git a/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol index f11b2304de..20fa61ab58 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IUniswapExchange.sol @@ -47,7 +47,6 @@ interface IUniswapExchange { uint256 deadline ) external - payable returns (uint256 ethBought); /// @dev Buys at least `minTokensBought` tokens with the exchange token @@ -68,4 +67,11 @@ interface IUniswapExchange { ) external returns (uint256 tokensBought); + + /// @dev Retrieves the token that is associated with this exchange. + /// @return tokenAddress The token address. + function toTokenAddress() + external + view + returns (address tokenAddress); } diff --git a/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol index a6ab535f82..19c4e86042 100644 --- a/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestUniswapBridge.sol @@ -26,15 +26,9 @@ import "../src/interfaces/IUniswapExchangeFactory.sol"; import "../src/interfaces/IUniswapExchange.sol"; +// solhint-disable no-simple-event-func-name contract TestEventsRaiser { - event SellAllAmount( - address sellToken, - uint256 sellTokenAmount, - address buyToken, - uint256 minimumFillAmount - ); - event TokenTransfer( address token, address from, @@ -56,18 +50,21 @@ contract TestEventsRaiser { ); event EthToTokenTransferInput( + address exchange, uint256 minTokensBought, uint256 deadline, address recipient ); event TokenToEthSwapInput( + address exchange, uint256 tokensSold, uint256 minEthBought, uint256 deadline ); event TokenToTokenTransferInput( + address exchange, uint256 tokensSold, uint256 minTokensBought, uint256 minEthBought, @@ -84,6 +81,7 @@ contract TestEventsRaiser { external { emit EthToTokenTransferInput( + msg.sender, minTokensBought, deadline, recipient @@ -98,6 +96,7 @@ contract TestEventsRaiser { external { emit TokenToEthSwapInput( + msg.sender, tokensSold, minEthBought, deadline @@ -115,6 +114,7 @@ contract TestEventsRaiser { external { emit TokenToTokenTransferInput( + msg.sender, tokensSold, minTokensBought, minEthBought, @@ -158,21 +158,38 @@ contract TestEventsRaiser { } } + /// @dev A minimalist ERC20/WETH token. contract TestToken { using LibSafeMath for uint256; mapping (address => uint256) public balances; + string private _nextRevertReason; - /// @dev Calls `raiseTokenTransfer()` on the caller. + /// @dev Set the balance for `owner`. + function setBalance(address owner) + external + payable + { + balances[owner] = msg.value; + } + + /// @dev Set the revert reason for `transfer()`, + /// `deposit()`, and `withdraw()`. + function setRevertReason(string calldata reason) + external + { + _nextRevertReason = reason; + } + + /// @dev Just calls `raiseTokenTransfer()` on the caller. function transfer(address to, uint256 amount) external returns (bool) { + _revertIfReasonExists(); TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount); - balances[msg.sender] = balances[msg.sender].safeSub(amount); - balances[to] = balances[to].safeAdd(amount); return true; } @@ -185,20 +202,13 @@ contract TestToken { return true; } - /// @dev Set the balance for `owner`. - function setBalance(address owner, uint256 balance) - external - payable - { - balances[owner] = balance; - } - /// @dev `IWETH.deposit()` that increases balances and calls /// `raiseWethDeposit()` on the caller. function deposit() external payable { + _revertIfReasonExists(); balances[msg.sender] += balances[msg.sender].safeAdd(msg.value); TestEventsRaiser(msg.sender).raiseWethDeposit(msg.value); } @@ -208,6 +218,7 @@ contract TestToken { function withdraw(uint256 amount) external { + _revertIfReasonExists(); balances[msg.sender] = balances[msg.sender].safeSub(amount); msg.sender.transfer(amount); TestEventsRaiser(msg.sender).raiseWethWithdraw(amount); @@ -221,6 +232,15 @@ contract TestToken { { return balances[owner]; } + + function _revertIfReasonExists() + private + view + { + if (bytes(_nextRevertReason).length != 0) { + revert(_nextRevertReason); + } + } } @@ -229,21 +249,18 @@ contract TestExchange is { address public tokenAddress; string private _nextRevertReason; - uint256 private _nextFillAmount; constructor(address _tokenAddress) public { tokenAddress = _tokenAddress; } function setFillBehavior( - string calldata revertReason, - uint256 fillAmount + string calldata revertReason ) external payable { _nextRevertReason = revertReason; - _nextFillAmount = fillAmount; } function ethToTokenTransferInput( @@ -261,7 +278,7 @@ contract TestExchange is recipient ); _revertIfReasonExists(); - return _nextFillAmount; + return address(this).balance; } function tokenToEthSwapInput( @@ -270,7 +287,6 @@ contract TestExchange is uint256 deadline ) external - payable returns (uint256 ethBought) { TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput( @@ -279,7 +295,9 @@ contract TestExchange is deadline ); _revertIfReasonExists(); - return _nextFillAmount; + uint256 fillAmount = address(this).balance; + msg.sender.transfer(fillAmount); + return fillAmount; } function tokenToTokenTransferInput( @@ -302,11 +320,20 @@ contract TestExchange is toTokenAddress ); _revertIfReasonExists(); - return _nextFillAmount; + return address(this).balance; + } + + function toTokenAddress() + external + view + returns (address _tokenAddress) + { + return tokenAddress; } function _revertIfReasonExists() private + view { if (bytes(_nextRevertReason).length != 0) { revert(_nextRevertReason); @@ -321,50 +348,59 @@ contract TestUniswapBridge is TestEventsRaiser, UniswapBridge { - - TestToken public wethToken = new TestToken(); + TestToken public wethToken; // Token address to TestToken instance. mapping (address => TestToken) private _testTokens; // Token address to TestExchange instance. mapping (address => TestExchange) private _testExchanges; - /// @dev Set token balances for this contract. - function setTokenBalances(address tokenAddress, uint256 balance) - external - { - TestToken token = _testTokens[tokenAddress]; - // Create the token if it doesn't exist. - if (address(token) == address(0)) { - _testTokens[tokenAddress] = token = new TestToken(); - } - token.setBalance(address(this), balance); + constructor() public { + wethToken = new TestToken(); + _testTokens[address(wethToken)] = wethToken; } - /// @dev Set the behavior for a fill on a uniswap exchange. - function setExchangeFillBehavior( - address exchangeAddress, - string calldata revertReason, - uint256 fillAmount - ) + /// @dev Sets the balance of this contract for an existing token. + /// The wei attached will be the balance. + function setTokenBalance(address tokenAddress) external payable { - createExchange(exchangeAddress).setFillBehavior.value(msg.value)( - revertReason, - fillAmount - ); + TestToken token = _testTokens[tokenAddress]; + token.deposit.value(msg.value)(); } - /// @dev Create an exchange for a token. - function createExchange(address tokenAddress) - public - returns (TestExchange exchangeAddress) + /// @dev Sets the revert reason for an existing token. + function setTokenRevertReason(address tokenAddress, string calldata revertReason) + external { - TestExchange exchange = _testExchanges[tokenAddress]; - if (address(exchange) == address(0)) { - _testExchanges[tokenAddress] = exchange = new TestExchange(tokenAddress); + TestToken token = _testTokens[tokenAddress]; + token.setRevertReason(revertReason); + } + + /// @dev Create a token and exchange (if they don't exist) for a new token + /// and sets the exchange revert and fill behavior. The wei attached + /// will be the fill amount for the exchange. + /// @param tokenAddress The token address. If zero, one will be created. + /// @param revertReason The revert reason for exchange operations. + function createTokenAndExchange( + address tokenAddress, + string calldata revertReason + ) + external + payable + returns (TestToken token, TestExchange exchange) + { + token = TestToken(tokenAddress); + if (tokenAddress == address(0)) { + token = new TestToken(); } - return exchange; + _testTokens[address(token)] = token; + exchange = _testExchanges[address(token)]; + if (address(exchange) == address(0)) { + _testExchanges[address(token)] = exchange = new TestExchange(address(token)); + } + exchange.setFillBehavior.value(msg.value)(revertReason); + return (token, exchange); } /// @dev `IUniswapExchangeFactory.getExchange` diff --git a/contracts/asset-proxy/test/uniswap_bridge.ts b/contracts/asset-proxy/test/uniswap_bridge.ts index 2d236cd1c0..d2dd2de3d6 100644 --- a/contracts/asset-proxy/test/uniswap_bridge.ts +++ b/contracts/asset-proxy/test/uniswap_bridge.ts @@ -2,13 +2,16 @@ 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'; @@ -16,15 +19,19 @@ import * as _ from 'lodash'; import { artifacts, TestUniswapBridgeContract, - TestUniswapBridgeEvents, - TestUniswapBridgeSellAllAmountEventArgs, - TestUniswapBridgeTokenTransferEventArgs, + 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 => { +blockchainTests.resets.only('UniswapBridge unit tests', env => { const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); let testContract: TestUniswapBridgeContract; - let daiTokenAddress: string; let wethTokenAddress: string; before(async () => { @@ -34,18 +41,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => { env.txDefaults, artifacts, ); - [daiTokenAddress, wethTokenAddress] = await Promise.all([ - testContract.daiToken.callAsync(), - testContract.wethToken.callAsync(), - ]); - }); - - describe('deployment', () => { - it('sets Uniswap allowances to maximum', async () => { - const [wethAllowance, daiAllowance] = await testContract.getUniswapTokenAllowances.callAsync(); - expect(wethAllowance).to.bignumber.eq(constants.MAX_UINT256); - expect(daiAllowance).to.bignumber.eq(constants.MAX_UINT256); - }); + wethTokenAddress = await testContract.wethToken.callAsync(); }); describe('isValidSignature()', () => { @@ -56,125 +52,333 @@ blockchainTests.resets('UniswapBridge unit tests', env => { }); }); - describe('transfer()', () => { - interface TransferOpts { + describe('withdrawTo()', () => { + interface WithdrawToOpts { + fromTokenAddress: string; toTokenAddress: string; + fromTokenBalance: Numberish; toAddress: string; amount: Numberish; - fromTokenBalance: Numberish; - revertReason: string; - fillAmount: Numberish; + exchangeRevertReason: string; + exchangeFillAmount: Numberish; + toTokenRevertReason: string; + fromTokenRevertReason: string; } - function createTransferOpts(opts?: Partial): TransferOpts { + function createWithdrawToOpts(opts?: Partial): WithdrawToOpts { return { - toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0], + fromTokenAddress: constants.NULL_ADDRESS, + toTokenAddress: constants.NULL_ADDRESS, + fromTokenBalance: getRandomInteger(1, 1e18), toAddress: randomAddress(), - amount: getRandomInteger(1, 100e18), - revertReason: '', - fillAmount: getRandomInteger(1, 100e18), - fromTokenBalance: getRandomInteger(1, 100e18), + amount: getRandomInteger(1, 1e18), + exchangeRevertReason: '', + exchangeFillAmount: getRandomInteger(1, 1e18), + toTokenRevertReason: '', + fromTokenRevertReason: '', ...opts, }; } - async function transferAsync(opts?: Partial): Promise<[string, DecodedLogs]> { - const _opts = createTransferOpts(opts); - // Set the fill behavior. - await testContract.setFillBehavior.awaitTransactionSuccessAsync( - _opts.revertReason, - new BigNumber(_opts.fillAmount), - ); - // Set the token balance for the token we're converting from. - await testContract.setTokenBalances.awaitTransactionSuccessAsync( - _opts.toTokenAddress === daiTokenAddress - ? new BigNumber(_opts.fromTokenBalance) - : constants.ZERO_AMOUNT, - _opts.toTokenAddress === wethTokenAddress - ? new BigNumber(_opts.fromTokenBalance) - : constants.ZERO_AMOUNT, - ); - // Call transfer(). - const [result, { logs }] = await txHelper.getResultAndReceiptAsync( - testContract.transfer, - '0x', - _opts.toTokenAddress, - randomAddress(), - _opts.toAddress, - new BigNumber(_opts.amount), - ); - return [result, (logs as any) as DecodedLogs]; + interface WithdrawToResult { + opts: WithdrawToOpts; + result: string; + logs: DecodedLogs; + blockTime: number; } - function getOppositeToken(tokenAddress: string): string { - if (tokenAddress === daiTokenAddress) { - return wethTokenAddress; - } - return daiTokenAddress; + 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, + 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 BRIDGE_SUCCESS_RETURN_DATA = '0xb5d40d78'; - const [result] = await transferAsync(); - expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA); + const { result } = await withdrawToAsync(); + expect(result).to.eq(AssetProxyId.ERC20Bridge); }); - it('calls `Uniswap.sellAllAmount()`', async () => { - const opts = createTransferOpts(); - const [, logs] = await transferAsync(opts); - const transfers = filterLogsToArguments( - logs, - TestUniswapBridgeEvents.SellAllAmount, + 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].sellToken).to.eq(getOppositeToken(opts.toTokenAddress)); - expect(transfers[0].buyToken).to.eq(opts.toTokenAddress); - expect(transfers[0].sellTokenAmount).to.bignumber.eq(opts.fromTokenBalance); - expect(transfers[0].minimumFillAmount).to.bignumber.eq(opts.amount); - }); - - it('can swap DAI for WETH', async () => { - const opts = createTransferOpts({ toTokenAddress: wethTokenAddress }); - const [, logs] = await transferAsync(opts); - const transfers = filterLogsToArguments( - logs, - TestUniswapBridgeEvents.SellAllAmount, - ); - expect(transfers.length).to.eq(1); - expect(transfers[0].sellToken).to.eq(daiTokenAddress); - expect(transfers[0].buyToken).to.eq(wethTokenAddress); - }); - - it('can swap WETH for DAI', async () => { - const opts = createTransferOpts({ toTokenAddress: daiTokenAddress }); - const [, logs] = await transferAsync(opts); - const transfers = filterLogsToArguments( - logs, - TestUniswapBridgeEvents.SellAllAmount, - ); - expect(transfers.length).to.eq(1); - expect(transfers[0].sellToken).to.eq(wethTokenAddress); - expect(transfers[0].buyToken).to.eq(daiTokenAddress); - }); - - it('transfers filled amount to `to`', async () => { - const opts = createTransferOpts(); - const [, logs] = await transferAsync(opts); - const transfers = filterLogsToArguments( - logs, - TestUniswapBridgeEvents.TokenTransfer, - ); - expect(transfers.length).to.eq(1); - expect(transfers[0].token).to.eq(opts.toTokenAddress); + 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.fillAmount); + expect(transfers[0].amount).to.bignumber.eq(opts.amount); }); - it('fails if `Uniswap.sellAllAmount()` reverts', async () => { - const opts = createTransferOpts({ revertReason: 'FOOBAR' }); - const tx = transferAsync(opts); - return expect(tx).to.revertWith(opts.revertReason); + 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(0); + 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 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 only once', async () => { + const { opts } = await withdrawToAsync(); + const { logs } = await withdrawToAsync(opts); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); + expect(transfers.length).to.eq(0); + }); + + 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(0); + 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('calls `IUniswapExchange.tokenToEthSwapInput()`', async () => { + const { opts, logs, blockTime } = await withdrawToAsync({ + toTokenAddress: wethTokenAddress, + }); + const calls = filterLogsToArguments(logs, ContractEvents.TokenToEthSwapInput); + const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress); + 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].minEthBought).to.bignumber.eq(0); + expect(calls[0].deadline).to.bignumber.eq(blockTime); + }); + + 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 only once', async () => { + const { opts } = await withdrawToAsync({ + toTokenAddress: wethTokenAddress, + }); + const { logs } = await withdrawToAsync(opts); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); + expect(transfers.length).to.eq(0); + }); + + 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(0); + expect(calls[0].args.deadline).to.bignumber.eq(blockTime); + expect(calls[0].args.recipient).to.eq(opts.toAddress); + }); + + it('sets allowance for "to" token', async () => { + const { opts, logs } = await withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + }); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const exchangeAddress = await getExchangeForTokenAsync(opts.toTokenAddress); + 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 only', async () => { + const { opts } = await withdrawToAsync({ + fromTokenAddress: wethTokenAddress, + }); + const { logs } = await withdrawToAsync(opts); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenApprove); + expect(transfers.length).to.eq(0); + }); + + 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); + }); }); }); });