@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/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;
}
}

View File

@ -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);
}

View File

@ -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`

View File

@ -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>): TransferOpts {
function createWithdrawToOpts(opts?: Partial<WithdrawToOpts>): 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<TransferOpts>): 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<WithdrawToOpts>): Promise<WithdrawToResult> {
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<string> {
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<TestUniswapBridgeSellAllAmountEventArgs>(
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<TokenTransferArgs>(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<TestUniswapBridgeSellAllAmountEventArgs>(
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<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].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<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);
});
});
});
});