@0x/contracts-asset-proxy: Finish off UniswapBridge tests.

This commit is contained in:
Lawrence Forman 2019-10-01 18:15:33 -07:00
parent b383781870
commit c2261a6bbe
4 changed files with 437 additions and 195 deletions

View File

@ -21,18 +21,18 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.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/IUniswapExchangeFactory.sol";
import "../interfaces/IUniswapExchange.sol"; import "../interfaces/IUniswapExchange.sol";
import "./ERC20Bridge.sol"; import "../interfaces/IWallet.sol";
import "../interfaces/IERC20Bridge.sol";
// solhint-disable space-after-comma // solhint-disable space-after-comma
// solhint-disable not-rely-on-time
contract UniswapBridge is contract UniswapBridge is
ERC20Bridge, IERC20Bridge,
IWallet IWallet
{ {
bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381;
/* Mainnet addresses */ /* Mainnet addresses */
address constant public UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; address constant public UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
@ -45,8 +45,15 @@ contract UniswapBridge is
IEtherToken weth; IEtherToken weth;
} }
/// @dev Whether we've granted an allowance to a spender for a token. // solhint-disable no-empty-blocks
mapping (address => mapping (address => bool)) private _hasAllowance; /// @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 /// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of
/// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress` /// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress`
@ -83,7 +90,7 @@ contract UniswapBridge is
toTokenAddress toTokenAddress
); );
// Grant an allowance to the exchange. // Grant an allowance to the exchange.
_grantAllowanceForTokens(address(state.exchange), [fromTokenAddress, toTokenAddress]); _grantExchangeAllowance(state.exchange);
// Get our balance of `fromTokenAddress` token. // Get our balance of `fromTokenAddress` token.
state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this));
// Get the weth contract. // Get the weth contract.
@ -176,27 +183,16 @@ contract UniswapBridge is
return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS); return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS);
} }
/// @dev Grants an unlimited allowance to `spender` for the tokens passed, /// @dev Grants an unlimited allowance to the exchange for its token
/// if they're not WETH and we haven't already granted `spender` an /// on behalf of this contract, if we haven't already done so.
/// allowance. /// @param exchange The Uniswap token exchange.
/// @param spender The spender being granted an aloowance. function _grantExchangeAllowance(IUniswapExchange exchange)
/// @param tokenAddresses Array of token addresses.
function _grantAllowanceForTokens(
address spender,
address[2] memory tokenAddresses
)
private private
{ {
address wethAddress = address(_getWethContract()); address tokenAddress = exchange.toTokenAddress();
mapping (address => bool) storage doesSpenderHaveAllowance = _hasAllowance[spender]; if (!_hasAllowance[tokenAddress]) {
for (uint256 i = 0; i < tokenAddresses.length; ++i) { IERC20Token(tokenAddress).approve(address(exchange), uint256(-1));
address tokenAddress = tokenAddresses[i]; _hasAllowance[tokenAddress] = true;
if (tokenAddress != wethAddress) {
if (!doesSpenderHaveAllowance[tokenAddress]) {
IERC20Token(tokenAddress).approve(spender, uint256(-1));
doesSpenderHaveAllowance[tokenAddress] = true;
}
}
} }
} }

View File

@ -47,7 +47,6 @@ interface IUniswapExchange {
uint256 deadline uint256 deadline
) )
external external
payable
returns (uint256 ethBought); returns (uint256 ethBought);
/// @dev Buys at least `minTokensBought` tokens with the exchange token /// @dev Buys at least `minTokensBought` tokens with the exchange token
@ -68,4 +67,11 @@ interface IUniswapExchange {
) )
external external
returns (uint256 tokensBought); 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);
} }

View File

@ -26,15 +26,9 @@ import "../src/interfaces/IUniswapExchangeFactory.sol";
import "../src/interfaces/IUniswapExchange.sol"; import "../src/interfaces/IUniswapExchange.sol";
// solhint-disable no-simple-event-func-name
contract TestEventsRaiser { contract TestEventsRaiser {
event SellAllAmount(
address sellToken,
uint256 sellTokenAmount,
address buyToken,
uint256 minimumFillAmount
);
event TokenTransfer( event TokenTransfer(
address token, address token,
address from, address from,
@ -56,18 +50,21 @@ contract TestEventsRaiser {
); );
event EthToTokenTransferInput( event EthToTokenTransferInput(
address exchange,
uint256 minTokensBought, uint256 minTokensBought,
uint256 deadline, uint256 deadline,
address recipient address recipient
); );
event TokenToEthSwapInput( event TokenToEthSwapInput(
address exchange,
uint256 tokensSold, uint256 tokensSold,
uint256 minEthBought, uint256 minEthBought,
uint256 deadline uint256 deadline
); );
event TokenToTokenTransferInput( event TokenToTokenTransferInput(
address exchange,
uint256 tokensSold, uint256 tokensSold,
uint256 minTokensBought, uint256 minTokensBought,
uint256 minEthBought, uint256 minEthBought,
@ -84,6 +81,7 @@ contract TestEventsRaiser {
external external
{ {
emit EthToTokenTransferInput( emit EthToTokenTransferInput(
msg.sender,
minTokensBought, minTokensBought,
deadline, deadline,
recipient recipient
@ -98,6 +96,7 @@ contract TestEventsRaiser {
external external
{ {
emit TokenToEthSwapInput( emit TokenToEthSwapInput(
msg.sender,
tokensSold, tokensSold,
minEthBought, minEthBought,
deadline deadline
@ -115,6 +114,7 @@ contract TestEventsRaiser {
external external
{ {
emit TokenToTokenTransferInput( emit TokenToTokenTransferInput(
msg.sender,
tokensSold, tokensSold,
minTokensBought, minTokensBought,
minEthBought, minEthBought,
@ -158,21 +158,38 @@ contract TestEventsRaiser {
} }
} }
/// @dev A minimalist ERC20/WETH token. /// @dev A minimalist ERC20/WETH token.
contract TestToken { contract TestToken {
using LibSafeMath for uint256; using LibSafeMath for uint256;
mapping (address => uint256) public balances; 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) function transfer(address to, uint256 amount)
external external
returns (bool) returns (bool)
{ {
_revertIfReasonExists();
TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount); TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount);
balances[msg.sender] = balances[msg.sender].safeSub(amount);
balances[to] = balances[to].safeAdd(amount);
return true; return true;
} }
@ -185,20 +202,13 @@ contract TestToken {
return true; 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 /// @dev `IWETH.deposit()` that increases balances and calls
/// `raiseWethDeposit()` on the caller. /// `raiseWethDeposit()` on the caller.
function deposit() function deposit()
external external
payable payable
{ {
_revertIfReasonExists();
balances[msg.sender] += balances[msg.sender].safeAdd(msg.value); balances[msg.sender] += balances[msg.sender].safeAdd(msg.value);
TestEventsRaiser(msg.sender).raiseWethDeposit(msg.value); TestEventsRaiser(msg.sender).raiseWethDeposit(msg.value);
} }
@ -208,6 +218,7 @@ contract TestToken {
function withdraw(uint256 amount) function withdraw(uint256 amount)
external external
{ {
_revertIfReasonExists();
balances[msg.sender] = balances[msg.sender].safeSub(amount); balances[msg.sender] = balances[msg.sender].safeSub(amount);
msg.sender.transfer(amount); msg.sender.transfer(amount);
TestEventsRaiser(msg.sender).raiseWethWithdraw(amount); TestEventsRaiser(msg.sender).raiseWethWithdraw(amount);
@ -221,6 +232,15 @@ contract TestToken {
{ {
return balances[owner]; return balances[owner];
} }
function _revertIfReasonExists()
private
view
{
if (bytes(_nextRevertReason).length != 0) {
revert(_nextRevertReason);
}
}
} }
@ -229,21 +249,18 @@ contract TestExchange is
{ {
address public tokenAddress; address public tokenAddress;
string private _nextRevertReason; string private _nextRevertReason;
uint256 private _nextFillAmount;
constructor(address _tokenAddress) public { constructor(address _tokenAddress) public {
tokenAddress = _tokenAddress; tokenAddress = _tokenAddress;
} }
function setFillBehavior( function setFillBehavior(
string calldata revertReason, string calldata revertReason
uint256 fillAmount
) )
external external
payable payable
{ {
_nextRevertReason = revertReason; _nextRevertReason = revertReason;
_nextFillAmount = fillAmount;
} }
function ethToTokenTransferInput( function ethToTokenTransferInput(
@ -261,7 +278,7 @@ contract TestExchange is
recipient recipient
); );
_revertIfReasonExists(); _revertIfReasonExists();
return _nextFillAmount; return address(this).balance;
} }
function tokenToEthSwapInput( function tokenToEthSwapInput(
@ -270,7 +287,6 @@ contract TestExchange is
uint256 deadline uint256 deadline
) )
external external
payable
returns (uint256 ethBought) returns (uint256 ethBought)
{ {
TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput( TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput(
@ -279,7 +295,9 @@ contract TestExchange is
deadline deadline
); );
_revertIfReasonExists(); _revertIfReasonExists();
return _nextFillAmount; uint256 fillAmount = address(this).balance;
msg.sender.transfer(fillAmount);
return fillAmount;
} }
function tokenToTokenTransferInput( function tokenToTokenTransferInput(
@ -302,11 +320,20 @@ contract TestExchange is
toTokenAddress toTokenAddress
); );
_revertIfReasonExists(); _revertIfReasonExists();
return _nextFillAmount; return address(this).balance;
}
function toTokenAddress()
external
view
returns (address _tokenAddress)
{
return tokenAddress;
} }
function _revertIfReasonExists() function _revertIfReasonExists()
private private
view
{ {
if (bytes(_nextRevertReason).length != 0) { if (bytes(_nextRevertReason).length != 0) {
revert(_nextRevertReason); revert(_nextRevertReason);
@ -321,50 +348,59 @@ contract TestUniswapBridge is
TestEventsRaiser, TestEventsRaiser,
UniswapBridge UniswapBridge
{ {
TestToken public wethToken;
TestToken public wethToken = new TestToken();
// Token address to TestToken instance. // Token address to TestToken instance.
mapping (address => TestToken) private _testTokens; mapping (address => TestToken) private _testTokens;
// Token address to TestExchange instance. // Token address to TestExchange instance.
mapping (address => TestExchange) private _testExchanges; mapping (address => TestExchange) private _testExchanges;
/// @dev Set token balances for this contract. constructor() public {
function setTokenBalances(address tokenAddress, uint256 balance) wethToken = new TestToken();
external _testTokens[address(wethToken)] = wethToken;
{
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);
} }
/// @dev Set the behavior for a fill on a uniswap exchange. /// @dev Sets the balance of this contract for an existing token.
function setExchangeFillBehavior( /// The wei attached will be the balance.
address exchangeAddress, function setTokenBalance(address tokenAddress)
string calldata revertReason,
uint256 fillAmount
)
external external
payable payable
{ {
createExchange(exchangeAddress).setFillBehavior.value(msg.value)( TestToken token = _testTokens[tokenAddress];
revertReason, token.deposit.value(msg.value)();
fillAmount
);
} }
/// @dev Create an exchange for a token. /// @dev Sets the revert reason for an existing token.
function createExchange(address tokenAddress) function setTokenRevertReason(address tokenAddress, string calldata revertReason)
public external
returns (TestExchange exchangeAddress)
{ {
TestExchange exchange = _testExchanges[tokenAddress]; TestToken token = _testTokens[tokenAddress];
if (address(exchange) == address(0)) { token.setRevertReason(revertReason);
_testExchanges[tokenAddress] = exchange = new TestExchange(tokenAddress);
} }
return exchange;
/// @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();
}
_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` /// @dev `IUniswapExchangeFactory.getExchange`

View File

@ -2,13 +2,16 @@ import {
blockchainTests, blockchainTests,
constants, constants,
expect, expect,
filterLogs,
filterLogsToArguments, filterLogsToArguments,
getRandomInteger, getRandomInteger,
hexLeftPad,
hexRandom, hexRandom,
Numberish, Numberish,
randomAddress, randomAddress,
TransactionHelper, TransactionHelper,
} from '@0x/contracts-test-utils'; } from '@0x/contracts-test-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { DecodedLogs } from 'ethereum-types'; import { DecodedLogs } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -16,15 +19,19 @@ import * as _ from 'lodash';
import { import {
artifacts, artifacts,
TestUniswapBridgeContract, TestUniswapBridgeContract,
TestUniswapBridgeEvents, TestUniswapBridgeEthToTokenTransferInputEventArgs as EthToTokenTransferInputArgs,
TestUniswapBridgeSellAllAmountEventArgs, TestUniswapBridgeEvents as ContractEvents,
TestUniswapBridgeTokenTransferEventArgs, TestUniswapBridgeTokenApproveEventArgs as TokenApproveArgs,
TestUniswapBridgeTokenToEthSwapInputEventArgs as TokenToEthSwapInputArgs,
TestUniswapBridgeTokenToTokenTransferInputEventArgs as TokenToTokenTransferInputArgs,
TestUniswapBridgeTokenTransferEventArgs as TokenTransferArgs,
TestUniswapBridgeWethDepositEventArgs as WethDepositArgs,
TestUniswapBridgeWethWithdrawEventArgs as WethWithdrawArgs,
} from '../src'; } from '../src';
blockchainTests.resets('UniswapBridge unit tests', env => { blockchainTests.resets.only('UniswapBridge unit tests', env => {
const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); const txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
let testContract: TestUniswapBridgeContract; let testContract: TestUniswapBridgeContract;
let daiTokenAddress: string;
let wethTokenAddress: string; let wethTokenAddress: string;
before(async () => { before(async () => {
@ -34,18 +41,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => {
env.txDefaults, env.txDefaults,
artifacts, artifacts,
); );
[daiTokenAddress, wethTokenAddress] = await Promise.all([ wethTokenAddress = await testContract.wethToken.callAsync();
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);
});
}); });
describe('isValidSignature()', () => { describe('isValidSignature()', () => {
@ -56,125 +52,333 @@ blockchainTests.resets('UniswapBridge unit tests', env => {
}); });
}); });
describe('transfer()', () => { describe('withdrawTo()', () => {
interface TransferOpts { interface WithdrawToOpts {
fromTokenAddress: string;
toTokenAddress: string; toTokenAddress: string;
fromTokenBalance: Numberish;
toAddress: string; toAddress: string;
amount: Numberish; amount: Numberish;
fromTokenBalance: Numberish; exchangeRevertReason: string;
revertReason: string; exchangeFillAmount: Numberish;
fillAmount: Numberish; toTokenRevertReason: string;
fromTokenRevertReason: string;
} }
function createTransferOpts(opts?: Partial<TransferOpts>): TransferOpts { function createWithdrawToOpts(opts?: Partial<WithdrawToOpts>): WithdrawToOpts {
return { return {
toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0], fromTokenAddress: constants.NULL_ADDRESS,
toTokenAddress: constants.NULL_ADDRESS,
fromTokenBalance: getRandomInteger(1, 1e18),
toAddress: randomAddress(), toAddress: randomAddress(),
amount: getRandomInteger(1, 100e18), amount: getRandomInteger(1, 1e18),
revertReason: '', exchangeRevertReason: '',
fillAmount: getRandomInteger(1, 100e18), exchangeFillAmount: getRandomInteger(1, 1e18),
fromTokenBalance: getRandomInteger(1, 100e18), toTokenRevertReason: '',
fromTokenRevertReason: '',
...opts, ...opts,
}; };
} }
async function transferAsync(opts?: Partial<TransferOpts>): Promise<[string, DecodedLogs]> { interface WithdrawToResult {
const _opts = createTransferOpts(opts); opts: WithdrawToOpts;
// Set the fill behavior. result: string;
await testContract.setFillBehavior.awaitTransactionSuccessAsync( logs: DecodedLogs;
_opts.revertReason, blockTime: number;
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];
} }
function getOppositeToken(tokenAddress: string): string { async function withdrawToAsync(opts?: Partial<WithdrawToOpts>): Promise<WithdrawToResult> {
if (tokenAddress === daiTokenAddress) { const _opts = createWithdrawToOpts(opts);
return wethTokenAddress; // 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),
};
} }
return daiTokenAddress;
async function getExchangeForTokenAsync(tokenAddress: string): Promise<string> {
return testContract.getExchange.callAsync(tokenAddress);
} }
it('returns magic bytes on success', async () => { it('returns magic bytes on success', async () => {
const BRIDGE_SUCCESS_RETURN_DATA = '0xb5d40d78'; const { result } = await withdrawToAsync();
const [result] = await transferAsync(); expect(result).to.eq(AssetProxyId.ERC20Bridge);
expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA);
}); });
it('calls `Uniswap.sellAllAmount()`', async () => { it('just transfers tokens to `to` if the same tokens are in play', async () => {
const opts = createTransferOpts(); const [[tokenAddress]] = await txHelper.getResultAndReceiptAsync(
const [, logs] = await transferAsync(opts); testContract.createTokenAndExchange,
const transfers = filterLogsToArguments<TestUniswapBridgeSellAllAmountEventArgs>( constants.NULL_ADDRESS,
logs, '',
TestUniswapBridgeEvents.SellAllAmount,
); );
expect(transfers.length).to.eq(1); const { opts, result, logs } = await withdrawToAsync({
expect(transfers[0].sellToken).to.eq(getOppositeToken(opts.toTokenAddress)); fromTokenAddress: tokenAddress,
expect(transfers[0].buyToken).to.eq(opts.toTokenAddress); toTokenAddress: tokenAddress,
expect(transfers[0].sellTokenAmount).to.bignumber.eq(opts.fromTokenBalance);
expect(transfers[0].minimumFillAmount).to.bignumber.eq(opts.amount);
}); });
expect(result).to.eq(AssetProxyId.ERC20Bridge);
it('can swap DAI for WETH', async () => { const transfers = filterLogsToArguments<TokenTransferArgs>(logs, ContractEvents.TokenTransfer);
const opts = createTransferOpts({ toTokenAddress: wethTokenAddress });
const [, logs] = await transferAsync(opts);
const transfers = filterLogsToArguments<TestUniswapBridgeSellAllAmountEventArgs>(
logs,
TestUniswapBridgeEvents.SellAllAmount,
);
expect(transfers.length).to.eq(1); expect(transfers.length).to.eq(1);
expect(transfers[0].sellToken).to.eq(daiTokenAddress); expect(transfers[0].token).to.eq(tokenAddress);
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<TestUniswapBridgeSellAllAmountEventArgs>(
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<TestUniswapBridgeTokenTransferEventArgs>(
logs,
TestUniswapBridgeEvents.TokenTransfer,
);
expect(transfers.length).to.eq(1);
expect(transfers[0].token).to.eq(opts.toTokenAddress);
expect(transfers[0].from).to.eq(testContract.address); expect(transfers[0].from).to.eq(testContract.address);
expect(transfers[0].to).to.eq(opts.toAddress); 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 () => { describe('token -> token', () => {
const opts = createTransferOpts({ revertReason: 'FOOBAR' }); it('calls `IUniswapExchange.tokenToTokenTransferInput()', async () => {
const tx = transferAsync(opts); const { opts, logs, blockTime } = await withdrawToAsync();
return expect(tx).to.revertWith(opts.revertReason); const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
const calls = filterLogsToArguments<TokenToTokenTransferInputArgs>(
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<TokenApproveArgs>(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<TokenApproveArgs>(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<TokenToEthSwapInputArgs>(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<WethDepositArgs>(
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<TokenTransferArgs>(
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<TokenToEthSwapInputArgs>(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<TokenApproveArgs>(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<TokenApproveArgs>(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<WethWithdrawArgs>(logs, ContractEvents.WethWithdraw);
expect(calls.length).to.eq(1);
expect(calls[0].args.amount).to.bignumber.eq(opts.fromTokenBalance);
calls = filterLogs<EthToTokenTransferInputArgs>(
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<TokenApproveArgs>(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<TokenApproveArgs>(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);
});
}); });
}); });
}); });