@0x/contracts-asset-proxy: Switch Eth2DaiBridge to support arbitrary tokens.

`@0x/contracts-asset-proxy`: Support non-conformant tokens in Eth2DaiBridge
This commit is contained in:
Lawrence Forman 2019-09-30 16:08:42 -07:00
parent 48f7a24505
commit bb87c8e7b5
7 changed files with 277 additions and 177 deletions

View File

@ -20,9 +20,9 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-exchange/contracts/src/interfaces/IWallet.sol";
import "./ERC20Bridge.sol"; import "./ERC20Bridge.sol";
import "../interfaces/IEth2Dai.sol"; import "../interfaces/IEth2Dai.sol";
import "../interfaces/IWallet.sol";
// solhint-disable space-after-comma // solhint-disable space-after-comma
@ -30,17 +30,11 @@ contract Eth2DaiBridge is
ERC20Bridge, ERC20Bridge,
IWallet IWallet
{ {
bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381;
/* Mainnet addresses */ /* Mainnet addresses */
address constant public ETH2DAI_ADDRESS = 0x39755357759cE0d7f32dC8dC45414CCa409AE24e; address constant public ETH2DAI_ADDRESS = 0x39755357759cE0d7f32dC8dC45414CCa409AE24e;
address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant public DAI_ADDRESS = 0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359;
constructor() public { /// @dev Whether we've granted an allowance to a spender for a token.
// Grant the Eth2Dai contract unlimited weth and dai allowances. mapping (address => mapping (address => bool)) private _hasAllowance;
_getWethContract().approve(address(_getEth2DaiContract()), uint256(-1));
_getDaiContract().approve(address(_getEth2DaiContract()), uint256(-1));
}
/// @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 opposing asset /// `toTokenAddress` tokens by selling the entirety of the opposing asset
@ -49,38 +43,34 @@ contract Eth2DaiBridge is
/// @param toTokenAddress The token to give to `to` (either DAI or WETH). /// @param toTokenAddress The token to give to `to` (either DAI or WETH).
/// @param to The recipient of the bought tokens. /// @param to The recipient of the bought tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param bridgeData The abi-encoeded "from" token address.
/// @return success The magic bytes if successful. /// @return success The magic bytes if successful.
function withdrawTo( function withdrawTo(
address toTokenAddress, address toTokenAddress,
address /* from */, address /* from */,
address to, address to,
uint256 amount, uint256 amount,
bytes calldata /* bridgeData */ bytes calldata bridgeData
) )
external external
returns (bytes4 success) returns (bytes4 success)
{ {
// The "from" token is the opposite of the "to" token. // Decode the bridge data to get the `fromTokenAddress`.
IERC20Token fromToken = _getWethContract(); (address fromTokenAddress) = abi.decode(bridgeData, (address));
IERC20Token toToken = _getDaiContract();
// Swap them if necessary. IEth2Dai exchange = _getEth2DaiContract();
if (toTokenAddress == address(fromToken)) { // Grant an allowance to the exchange to spend `fromTokenAddress` token.
(fromToken, toToken) = (toToken, fromToken); _grantAllowanceForToken(address(exchange), fromTokenAddress);
} else {
require( // Try to sell all of this contract's `fromTokenAddress` token balance.
toTokenAddress == address(toToken),
"INVALID_ETH2DAI_TOKEN"
);
}
// Try to sell all of this contract's `fromToken` balance.
uint256 boughtAmount = _getEth2DaiContract().sellAllAmount( uint256 boughtAmount = _getEth2DaiContract().sellAllAmount(
address(fromToken), address(fromTokenAddress),
fromToken.balanceOf(address(this)), IERC20Token(fromTokenAddress).balanceOf(address(this)),
address(toToken), toTokenAddress,
amount amount
); );
// Transfer the converted `toToken`s to `to`. // Transfer the converted `toToken`s to `to`.
toToken.transfer(to, boughtAmount); _transferERC20Token(toTokenAddress, to, boughtAmount);
return BRIDGE_SUCCESS; return BRIDGE_SUCCESS;
} }
@ -98,26 +88,6 @@ contract Eth2DaiBridge is
return LEGACY_WALLET_MAGIC_VALUE; return LEGACY_WALLET_MAGIC_VALUE;
} }
/// @dev Overridable way to get the weth contract.
/// @return weth The WETH contract.
function _getWethContract()
internal
view
returns (IERC20Token weth)
{
return IERC20Token(WETH_ADDRESS);
}
/// @dev Overridable way to get the dai contract.
/// @return token The token contract.
function _getDaiContract()
internal
view
returns (IERC20Token token)
{
return IERC20Token(DAI_ADDRESS);
}
/// @dev Overridable way to get the eth2dai contract. /// @dev Overridable way to get the eth2dai contract.
/// @return exchange The Eth2Dai exchange contract. /// @return exchange The Eth2Dai exchange contract.
function _getEth2DaiContract() function _getEth2DaiContract()
@ -127,4 +97,66 @@ contract Eth2DaiBridge is
{ {
return IEth2Dai(ETH2DAI_ADDRESS); return IEth2Dai(ETH2DAI_ADDRESS);
} }
/// @dev Grants an unlimited allowance to `spender` for `tokenAddress` token,
/// if we haven't done so already.
/// @param spender The spender address.
/// @param tokenAddress The token address.
function _grantAllowanceForToken(
address spender,
address tokenAddress
)
private
{
mapping (address => bool) storage spenderHasAllowance = _hasAllowance[spender];
if (!spenderHasAllowance[tokenAddress]) {
spenderHasAllowance[tokenAddress] = true;
IERC20Token(tokenAddress).approve(spender, uint256(-1));
}
}
/// @dev Permissively transfers an ERC20 token that may not adhere to
/// specs.
/// @param tokenAddress The token contract address.
/// @param to The token recipient.
/// @param amount The amount of tokens to transfer.
function _transferERC20Token(
address tokenAddress,
address to,
uint256 amount
)
private
{
// Transfer tokens.
// We do a raw call so we can check the success separate
// from the return data.
(bool didSucceed, bytes memory returnData) = tokenAddress.call(
abi.encodeWithSelector(
IERC20Token(0).transfer.selector,
to,
amount
)
);
if (!didSucceed) {
assembly { revert(add(returnData, 0x20), mload(returnData)) }
}
// Check return data.
// If there is no return data, we assume the token incorrectly
// does not return a bool. In this case we expect it to revert
// on failure, which was handled above.
// If the token does return data, we require that it is a single
// value that evaluates to true.
assembly {
if returndatasize {
didSucceed := 0
if eq(returndatasize, 32) {
// First 64 bytes of memory are reserved scratch space
returndatacopy(0, 0, 32)
didSucceed := mload(0)
}
}
}
require(didSucceed, "ERC20_TRANSFER_FAILED");
}
} }

View File

@ -0,0 +1,38 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
contract IWallet {
bytes4 internal constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381;
/// @dev Validates a hash with the `Wallet` signature type.
/// @param hash Message hash that is signed.
/// @param signature Proof of signing.
/// @return magicValue `bytes4(0xb0671381)` if the signature check succeeds.
function isValidSignature(
bytes32 hash,
bytes calldata signature
)
external
view
returns (bytes4 magicValue);
}

View File

@ -25,15 +25,41 @@ import "../src/interfaces/IEth2Dai.sol";
// solhint-disable no-simple-event-func-name // solhint-disable no-simple-event-func-name
/// @dev Interface that allows `TestToken` to call `raiseTransferEvent` on contract TestEvents {
/// the `TestEth2DaiBridge` contract.
interface IRaiseTransferEvent { event TokenTransfer(
function raiseTransferEvent( address token,
address from,
address to,
uint256 amount
);
event TokenApprove(
address token,
address spender,
uint256 allowance
);
function raiseTokenTransfer(
address from, address from,
address to, address to,
uint256 amount uint256 amount
) )
external; external
{
emit TokenTransfer(
msg.sender,
from,
to,
amount
);
}
function raiseTokenApprove(address spender, uint256 allowance)
external
{
emit TokenApprove(msg.sender, spender, allowance);
}
} }
@ -41,15 +67,20 @@ interface IRaiseTransferEvent {
contract TestToken { contract TestToken {
mapping (address => uint256) public balances; mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowances; string private _nextTransferRevertReason;
bytes private _nextTransferReturnData;
/// @dev Just calls `raiseTransferEvent()` on the caller. /// @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)
{ {
IRaiseTransferEvent(msg.sender).raiseTransferEvent(msg.sender, to, amount); TestEvents(msg.sender).raiseTokenTransfer(msg.sender, to, amount);
return true; if (bytes(_nextTransferRevertReason).length != 0) {
revert(_nextTransferRevertReason);
}
bytes memory returnData = _nextTransferReturnData;
assembly { return(add(returnData, 0x20), mload(returnData)) }
} }
/// @dev Set the balance for `owner`. /// @dev Set the balance for `owner`.
@ -59,12 +90,23 @@ contract TestToken {
balances[owner] = balance; balances[owner] = balance;
} }
/// @dev Records allowance values. /// @dev Set the behavior of the `transfer()` call.
function setTransferBehavior(
string calldata revertReason,
bytes calldata returnData
)
external
{
_nextTransferRevertReason = revertReason;
_nextTransferReturnData = returnData;
}
/// @dev Just calls `raiseTokenApprove()` on the caller.
function approve(address spender, uint256 allowance) function approve(address spender, uint256 allowance)
external external
returns (bool) returns (bool)
{ {
allowances[msg.sender][spender] = allowance; TestEvents(msg.sender).raiseTokenApprove(spender, allowance);
return true; return true;
} }
@ -82,6 +124,7 @@ contract TestToken {
/// @dev Eth2DaiBridge overridden to mock tokens and /// @dev Eth2DaiBridge overridden to mock tokens and
/// implement IEth2Dai. /// implement IEth2Dai.
contract TestEth2DaiBridge is contract TestEth2DaiBridge is
TestEvents,
IEth2Dai, IEth2Dai,
Eth2DaiBridge Eth2DaiBridge
{ {
@ -92,24 +135,19 @@ contract TestEth2DaiBridge is
uint256 minimumFillAmount uint256 minimumFillAmount
); );
event TokenTransfer( mapping (address => TestToken) public testTokens;
address token,
address from,
address to,
uint256 amount
);
TestToken public wethToken = new TestToken();
TestToken public daiToken = new TestToken();
string private _nextRevertReason; string private _nextRevertReason;
uint256 private _nextFillAmount; uint256 private _nextFillAmount;
/// @dev Set token balances for this contract. /// @dev Create a token and set this contract's balance.
function setTokenBalances(uint256 wethBalance, uint256 daiBalance) function createToken(uint256 balance)
external external
returns (address tokenAddress)
{ {
wethToken.setBalance(address(this), wethBalance); TestToken token = new TestToken();
daiToken.setBalance(address(this), daiBalance); testTokens[address(token)] = token;
token.setBalance(address(this), balance);
return address(token);
} }
/// @dev Set the behavior for `IEth2Dai.sellAllAmount()`. /// @dev Set the behavior for `IEth2Dai.sellAllAmount()`.
@ -120,6 +158,17 @@ contract TestEth2DaiBridge is
_nextFillAmount = fillAmount; _nextFillAmount = fillAmount;
} }
/// @dev Set the behavior of a token's `transfer()`.
function setTransferBehavior(
address tokenAddress,
string calldata revertReason,
bytes calldata returnData
)
external
{
testTokens[tokenAddress].setTransferBehavior(revertReason, returnData);
}
/// @dev Implementation of `IEth2Dai.sellAllAmount()` /// @dev Implementation of `IEth2Dai.sellAllAmount()`
function sellAllAmount( function sellAllAmount(
address sellTokenAddress, address sellTokenAddress,
@ -142,50 +191,6 @@ contract TestEth2DaiBridge is
return _nextFillAmount; return _nextFillAmount;
} }
function raiseTransferEvent(
address from,
address to,
uint256 amount
)
external
{
emit TokenTransfer(
msg.sender,
from,
to,
amount
);
}
/// @dev Retrieves the allowances of the test tokens.
function getEth2DaiTokenAllowances()
external
view
returns (uint256 wethAllowance, uint256 daiAllowance)
{
wethAllowance = wethToken.allowances(address(this), address(this));
daiAllowance = daiToken.allowances(address(this), address(this));
return (wethAllowance, daiAllowance);
}
// @dev Use `wethToken`.
function _getWethContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(address(wethToken));
}
// @dev Use `daiToken`.
function _getDaiContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(address(daiToken));
}
// @dev This contract will double as the Eth2Dai contract. // @dev This contract will double as the Eth2Dai contract.
function _getEth2DaiContract() function _getEth2DaiContract()
internal internal

View File

@ -16,6 +16,7 @@ import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispat
import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json';
import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json'; import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json';
import * as IEth2Dai from '../generated-artifacts/IEth2Dai.json'; import * as IEth2Dai from '../generated-artifacts/IEth2Dai.json';
import * as IWallet from '../generated-artifacts/IWallet.json';
import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json';
import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json';
import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json';
@ -40,6 +41,7 @@ export const artifacts = {
IAuthorizable: IAuthorizable as ContractArtifact, IAuthorizable: IAuthorizable as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact,
IWallet: IWallet as ContractArtifact,
TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact,
TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact,
TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact,

View File

@ -14,6 +14,7 @@ export * from '../generated-wrappers/i_asset_proxy_dispatcher';
export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_erc20_bridge'; export * from '../generated-wrappers/i_erc20_bridge';
export * from '../generated-wrappers/i_eth2_dai'; export * from '../generated-wrappers/i_eth2_dai';
export * from '../generated-wrappers/i_wallet';
export * from '../generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../generated-wrappers/mixin_asset_proxy_dispatcher';
export * from '../generated-wrappers/mixin_authorizable'; export * from '../generated-wrappers/mixin_authorizable';
export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/multi_asset_proxy';

View File

@ -4,6 +4,7 @@ import {
expect, expect,
filterLogsToArguments, filterLogsToArguments,
getRandomInteger, getRandomInteger,
hexLeftPad,
hexRandom, hexRandom,
Numberish, Numberish,
randomAddress, randomAddress,
@ -18,14 +19,13 @@ import {
TestEth2DaiBridgeContract, TestEth2DaiBridgeContract,
TestEth2DaiBridgeEvents, TestEth2DaiBridgeEvents,
TestEth2DaiBridgeSellAllAmountEventArgs, TestEth2DaiBridgeSellAllAmountEventArgs,
TestEth2DaiBridgeTokenApproveEventArgs,
TestEth2DaiBridgeTokenTransferEventArgs, TestEth2DaiBridgeTokenTransferEventArgs,
} from '../src'; } from '../src';
blockchainTests.resets('Eth2DaiBridge unit tests', env => { blockchainTests.resets.only('Eth2DaiBridge unit tests', env => {
const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); const txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
let testContract: TestEth2DaiBridgeContract; let testContract: TestEth2DaiBridgeContract;
let daiTokenAddress: string;
let wethTokenAddress: string;
before(async () => { before(async () => {
testContract = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync( testContract = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync(
@ -34,18 +34,6 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => {
env.txDefaults, env.txDefaults,
artifacts, artifacts,
); );
[daiTokenAddress, wethTokenAddress] = await Promise.all([
testContract.daiToken.callAsync(),
testContract.wethToken.callAsync(),
]);
});
describe('deployment', () => {
it('sets Eth2Dai allowances to maximum', async () => {
const [wethAllowance, daiAllowance] = await testContract.getEth2DaiTokenAllowances.callAsync();
expect(wethAllowance).to.bignumber.eq(constants.MAX_UINT256);
expect(daiAllowance).to.bignumber.eq(constants.MAX_UINT256);
});
}); });
describe('isValidSignature()', () => { describe('isValidSignature()', () => {
@ -57,109 +45,126 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => {
}); });
describe('withdrawTo()', () => { describe('withdrawTo()', () => {
interface TransferOpts { interface WithdrawToOpts {
toTokenAddress: string; toTokenAddress?: string;
fromTokenAddress?: string;
toAddress: string; toAddress: string;
amount: Numberish; amount: Numberish;
fromTokenBalance: Numberish; fromTokenBalance: Numberish;
revertReason: string; revertReason: string;
fillAmount: Numberish; fillAmount: Numberish;
toTokentransferRevertReason: string;
toTokenTransferReturnData: string;
} }
function createTransferOpts(opts?: Partial<TransferOpts>): TransferOpts { interface WithdrawToResult {
opts: WithdrawToOpts;
result: string;
logs: DecodedLogs;
}
function createWithdrawToOpts(opts?: Partial<WithdrawToOpts>): WithdrawToOpts {
return { return {
toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0],
toAddress: randomAddress(), toAddress: randomAddress(),
amount: getRandomInteger(1, 100e18), amount: getRandomInteger(1, 100e18),
revertReason: '', revertReason: '',
fillAmount: getRandomInteger(1, 100e18), fillAmount: getRandomInteger(1, 100e18),
fromTokenBalance: getRandomInteger(1, 100e18), fromTokenBalance: getRandomInteger(1, 100e18),
toTokentransferRevertReason: '',
toTokenTransferReturnData: hexLeftPad(1),
...opts, ...opts,
}; };
} }
async function transferAsync(opts?: Partial<TransferOpts>): Promise<[string, DecodedLogs]> { async function withdrawToAsync(opts?: Partial<WithdrawToOpts>): Promise<WithdrawToResult> {
const _opts = createTransferOpts(opts); const _opts = createWithdrawToOpts(opts);
// Set the fill behavior. // Set the fill behavior.
await testContract.setFillBehavior.awaitTransactionSuccessAsync( await testContract.setFillBehavior.awaitTransactionSuccessAsync(
_opts.revertReason, _opts.revertReason,
new BigNumber(_opts.fillAmount), new BigNumber(_opts.fillAmount),
); );
// Set the token balance for the token we're converting from. // Create tokens and balances.
await testContract.setTokenBalances.awaitTransactionSuccessAsync( if (_opts.fromTokenAddress === undefined) {
_opts.toTokenAddress === daiTokenAddress [_opts.fromTokenAddress] = await txHelper.getResultAndReceiptAsync(
? new BigNumber(_opts.fromTokenBalance) testContract.createToken,
: constants.ZERO_AMOUNT, new BigNumber(_opts.fromTokenBalance),
_opts.toTokenAddress === wethTokenAddress );
? new BigNumber(_opts.fromTokenBalance) }
: constants.ZERO_AMOUNT, if (_opts.toTokenAddress === undefined) {
[_opts.toTokenAddress] = await txHelper.getResultAndReceiptAsync(
testContract.createToken,
constants.ZERO_AMOUNT,
);
}
// Set the transfer behavior of `toTokenAddress`.
await testContract.setTransferBehavior.awaitTransactionSuccessAsync(
_opts.toTokenAddress,
_opts.toTokentransferRevertReason,
_opts.toTokenTransferReturnData,
); );
// Call withdrawTo(). // Call withdrawTo().
const [result, { logs }] = await txHelper.getResultAndReceiptAsync( const [result, { logs }] = await txHelper.getResultAndReceiptAsync(
testContract.withdrawTo, testContract.withdrawTo,
// "to" token address
_opts.toTokenAddress, _opts.toTokenAddress,
// Random from address.
randomAddress(), randomAddress(),
// To address.
_opts.toAddress, _opts.toAddress,
new BigNumber(_opts.amount), new BigNumber(_opts.amount),
'0x', // ABI-encode the "from" token address as the bridge data.
hexLeftPad(_opts.fromTokenAddress as string),
); );
return [result, (logs as any) as DecodedLogs]; return {
} opts: _opts,
result,
function getOppositeToken(tokenAddress: string): string { logs: (logs as any) as DecodedLogs,
if (tokenAddress === daiTokenAddress) { };
return wethTokenAddress;
}
return daiTokenAddress;
} }
it('returns magic bytes on success', async () => { it('returns magic bytes on success', async () => {
const BRIDGE_SUCCESS_RETURN_DATA = '0xdc1600f3'; const BRIDGE_SUCCESS_RETURN_DATA = '0xdc1600f3';
const [result] = await transferAsync(); const { result } = await withdrawToAsync();
expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA); expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA);
}); });
it('calls `Eth2Dai.sellAllAmount()`', async () => { it('calls `Eth2Dai.sellAllAmount()`', async () => {
const opts = createTransferOpts(); const { opts, logs } = await withdrawToAsync();
const [, logs] = await transferAsync(opts);
const transfers = filterLogsToArguments<TestEth2DaiBridgeSellAllAmountEventArgs>( const transfers = filterLogsToArguments<TestEth2DaiBridgeSellAllAmountEventArgs>(
logs, logs,
TestEth2DaiBridgeEvents.SellAllAmount, TestEth2DaiBridgeEvents.SellAllAmount,
); );
expect(transfers.length).to.eq(1); expect(transfers.length).to.eq(1);
expect(transfers[0].sellToken).to.eq(getOppositeToken(opts.toTokenAddress)); expect(transfers[0].sellToken).to.eq(opts.fromTokenAddress);
expect(transfers[0].buyToken).to.eq(opts.toTokenAddress); expect(transfers[0].buyToken).to.eq(opts.toTokenAddress);
expect(transfers[0].sellTokenAmount).to.bignumber.eq(opts.fromTokenBalance); expect(transfers[0].sellTokenAmount).to.bignumber.eq(opts.fromTokenBalance);
expect(transfers[0].minimumFillAmount).to.bignumber.eq(opts.amount); expect(transfers[0].minimumFillAmount).to.bignumber.eq(opts.amount);
}); });
it('can swap DAI for WETH', async () => { it('sets an unlimited allowance on the `fromTokenAddress` token', async () => {
const opts = createTransferOpts({ toTokenAddress: wethTokenAddress }); const { opts, logs } = await withdrawToAsync();
const [, logs] = await transferAsync(opts); const approvals = filterLogsToArguments<TestEth2DaiBridgeTokenApproveEventArgs>(
const transfers = filterLogsToArguments<TestEth2DaiBridgeSellAllAmountEventArgs>(
logs, logs,
TestEth2DaiBridgeEvents.SellAllAmount, TestEth2DaiBridgeEvents.TokenApprove,
); );
expect(transfers.length).to.eq(1); expect(approvals.length).to.eq(1);
expect(transfers[0].sellToken).to.eq(daiTokenAddress); expect(approvals[0].token).to.eq(opts.fromTokenAddress);
expect(transfers[0].buyToken).to.eq(wethTokenAddress); expect(approvals[0].spender).to.eq(testContract.address);
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
}); });
it('can swap WETH for DAI', async () => { it('does not set an unlimited allowance on the `fromTokenAddress` token if already set', async () => {
const opts = createTransferOpts({ toTokenAddress: daiTokenAddress }); const { opts } = await withdrawToAsync();
const [, logs] = await transferAsync(opts); const { logs } = await withdrawToAsync({ fromTokenAddress: opts.fromTokenAddress });
const transfers = filterLogsToArguments<TestEth2DaiBridgeSellAllAmountEventArgs>( const approvals = filterLogsToArguments<TestEth2DaiBridgeTokenApproveEventArgs>(
logs, logs,
TestEth2DaiBridgeEvents.SellAllAmount, TestEth2DaiBridgeEvents.TokenApprove,
); );
expect(transfers.length).to.eq(1); expect(approvals.length).to.eq(0);
expect(transfers[0].sellToken).to.eq(wethTokenAddress);
expect(transfers[0].buyToken).to.eq(daiTokenAddress);
}); });
it('transfers filled amount to `to`', async () => { it('transfers filled amount to `to`', async () => {
const opts = createTransferOpts(); const { opts, logs } = await withdrawToAsync();
const [, logs] = await transferAsync(opts);
const transfers = filterLogsToArguments<TestEth2DaiBridgeTokenTransferEventArgs>( const transfers = filterLogsToArguments<TestEth2DaiBridgeTokenTransferEventArgs>(
logs, logs,
TestEth2DaiBridgeEvents.TokenTransfer, TestEth2DaiBridgeEvents.TokenTransfer,
@ -172,9 +177,25 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => {
}); });
it('fails if `Eth2Dai.sellAllAmount()` reverts', async () => { it('fails if `Eth2Dai.sellAllAmount()` reverts', async () => {
const opts = createTransferOpts({ revertReason: 'FOOBAR' }); const opts = createWithdrawToOpts({ revertReason: 'FOOBAR' });
const tx = transferAsync(opts); const tx = withdrawToAsync(opts);
return expect(tx).to.revertWith(opts.revertReason); return expect(tx).to.revertWith(opts.revertReason);
}); });
it('fails if `toTokenAddress.transfer()` reverts', async () => {
const opts = createWithdrawToOpts({ toTokentransferRevertReason: 'FOOBAR' });
const tx = withdrawToAsync(opts);
return expect(tx).to.revertWith(opts.toTokentransferRevertReason);
});
it('fails if `toTokenAddress.transfer()` returns falsey', async () => {
const opts = createWithdrawToOpts({ toTokenTransferReturnData: hexLeftPad(0) });
const tx = withdrawToAsync(opts);
return expect(tx).to.revertWith('ERC20_TRANSFER_FAILED');
});
it('succeeds if `toTokenAddress.transfer()` returns truthy', async () => {
await withdrawToAsync({ toTokenTransferReturnData: hexLeftPad(100) });
});
}); });
}); });

View File

@ -14,6 +14,7 @@
"generated-artifacts/IAuthorizable.json", "generated-artifacts/IAuthorizable.json",
"generated-artifacts/IERC20Bridge.json", "generated-artifacts/IERC20Bridge.json",
"generated-artifacts/IEth2Dai.json", "generated-artifacts/IEth2Dai.json",
"generated-artifacts/IWallet.json",
"generated-artifacts/MixinAssetProxyDispatcher.json", "generated-artifacts/MixinAssetProxyDispatcher.json",
"generated-artifacts/MixinAuthorizable.json", "generated-artifacts/MixinAuthorizable.json",
"generated-artifacts/MultiAssetProxy.json", "generated-artifacts/MultiAssetProxy.json",