diff --git a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol index fe9fd5807d..0dcf9f3e10 100644 --- a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol @@ -20,9 +20,9 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; -import "@0x/contracts-exchange/contracts/src/interfaces/IWallet.sol"; import "./ERC20Bridge.sol"; import "../interfaces/IEth2Dai.sol"; +import "../interfaces/IWallet.sol"; // solhint-disable space-after-comma @@ -30,17 +30,11 @@ contract Eth2DaiBridge is ERC20Bridge, IWallet { - bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381; /* Mainnet addresses */ address constant public ETH2DAI_ADDRESS = 0x39755357759cE0d7f32dC8dC45414CCa409AE24e; - address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address constant public DAI_ADDRESS = 0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359; - constructor() public { - // Grant the Eth2Dai contract unlimited weth and dai allowances. - _getWethContract().approve(address(_getEth2DaiContract()), uint256(-1)); - _getDaiContract().approve(address(_getEth2DaiContract()), uint256(-1)); - } + /// @dev Whether we've granted an allowance to a spender for a token. + mapping (address => mapping (address => bool)) private _hasAllowance; /// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of /// `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 to The recipient of the bought tokens. /// @param amount Minimum amount of `toTokenAddress` tokens to buy. + /// @param bridgeData The abi-encoeded "from" token address. /// @return success The magic bytes if successful. function withdrawTo( address toTokenAddress, address /* from */, address to, uint256 amount, - bytes calldata /* bridgeData */ + bytes calldata bridgeData ) external returns (bytes4 success) { - // The "from" token is the opposite of the "to" token. - IERC20Token fromToken = _getWethContract(); - IERC20Token toToken = _getDaiContract(); - // Swap them if necessary. - if (toTokenAddress == address(fromToken)) { - (fromToken, toToken) = (toToken, fromToken); - } else { - require( - toTokenAddress == address(toToken), - "INVALID_ETH2DAI_TOKEN" - ); - } - // Try to sell all of this contract's `fromToken` balance. + // Decode the bridge data to get the `fromTokenAddress`. + (address fromTokenAddress) = abi.decode(bridgeData, (address)); + + IEth2Dai exchange = _getEth2DaiContract(); + // Grant an allowance to the exchange to spend `fromTokenAddress` token. + _grantAllowanceForToken(address(exchange), fromTokenAddress); + + // Try to sell all of this contract's `fromTokenAddress` token balance. uint256 boughtAmount = _getEth2DaiContract().sellAllAmount( - address(fromToken), - fromToken.balanceOf(address(this)), - address(toToken), + address(fromTokenAddress), + IERC20Token(fromTokenAddress).balanceOf(address(this)), + toTokenAddress, amount ); // Transfer the converted `toToken`s to `to`. - toToken.transfer(to, boughtAmount); + _transferERC20Token(toTokenAddress, to, boughtAmount); return BRIDGE_SUCCESS; } @@ -98,26 +88,6 @@ contract Eth2DaiBridge is 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. /// @return exchange The Eth2Dai exchange contract. function _getEth2DaiContract() @@ -127,4 +97,66 @@ contract Eth2DaiBridge is { 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"); + } } diff --git a/contracts/asset-proxy/contracts/src/interfaces/IWallet.sol b/contracts/asset-proxy/contracts/src/interfaces/IWallet.sol new file mode 100644 index 0000000000..2bb323e578 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IWallet.sol @@ -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); +} diff --git a/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol b/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol index 6774401e9d..2f1b07703e 100644 --- a/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestEth2DaiBridge.sol @@ -25,15 +25,41 @@ import "../src/interfaces/IEth2Dai.sol"; // solhint-disable no-simple-event-func-name -/// @dev Interface that allows `TestToken` to call `raiseTransferEvent` on -/// the `TestEth2DaiBridge` contract. -interface IRaiseTransferEvent { - function raiseTransferEvent( +contract TestEvents { + + event TokenTransfer( + address token, + address from, + address to, + uint256 amount + ); + + event TokenApprove( + address token, + address spender, + uint256 allowance + ); + + function raiseTokenTransfer( address from, address to, 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 { 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) external returns (bool) { - IRaiseTransferEvent(msg.sender).raiseTransferEvent(msg.sender, to, amount); - return true; + TestEvents(msg.sender).raiseTokenTransfer(msg.sender, to, amount); + if (bytes(_nextTransferRevertReason).length != 0) { + revert(_nextTransferRevertReason); + } + bytes memory returnData = _nextTransferReturnData; + assembly { return(add(returnData, 0x20), mload(returnData)) } } /// @dev Set the balance for `owner`. @@ -59,12 +90,23 @@ contract TestToken { 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) external returns (bool) { - allowances[msg.sender][spender] = allowance; + TestEvents(msg.sender).raiseTokenApprove(spender, allowance); return true; } @@ -82,6 +124,7 @@ contract TestToken { /// @dev Eth2DaiBridge overridden to mock tokens and /// implement IEth2Dai. contract TestEth2DaiBridge is + TestEvents, IEth2Dai, Eth2DaiBridge { @@ -92,24 +135,19 @@ contract TestEth2DaiBridge is uint256 minimumFillAmount ); - event TokenTransfer( - address token, - address from, - address to, - uint256 amount - ); - - TestToken public wethToken = new TestToken(); - TestToken public daiToken = new TestToken(); + mapping (address => TestToken) public testTokens; string private _nextRevertReason; uint256 private _nextFillAmount; - /// @dev Set token balances for this contract. - function setTokenBalances(uint256 wethBalance, uint256 daiBalance) + /// @dev Create a token and set this contract's balance. + function createToken(uint256 balance) external + returns (address tokenAddress) { - wethToken.setBalance(address(this), wethBalance); - daiToken.setBalance(address(this), daiBalance); + TestToken token = new TestToken(); + testTokens[address(token)] = token; + token.setBalance(address(this), balance); + return address(token); } /// @dev Set the behavior for `IEth2Dai.sellAllAmount()`. @@ -120,6 +158,17 @@ contract TestEth2DaiBridge is _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()` function sellAllAmount( address sellTokenAddress, @@ -142,50 +191,6 @@ contract TestEth2DaiBridge is 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. function _getEth2DaiContract() internal diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index 1296b11a1d..6662a863f5 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -16,6 +16,7 @@ import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispat import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.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 MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; @@ -40,6 +41,7 @@ export const artifacts = { IAuthorizable: IAuthorizable as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, + IWallet: IWallet as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 4731de6c12..a87eda1f77 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -14,6 +14,7 @@ export * from '../generated-wrappers/i_asset_proxy_dispatcher'; export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_erc20_bridge'; 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_authorizable'; export * from '../generated-wrappers/multi_asset_proxy'; diff --git a/contracts/asset-proxy/test/eth2dai_bridge.ts b/contracts/asset-proxy/test/eth2dai_bridge.ts index 418431bd46..ff851451ea 100644 --- a/contracts/asset-proxy/test/eth2dai_bridge.ts +++ b/contracts/asset-proxy/test/eth2dai_bridge.ts @@ -4,6 +4,7 @@ import { expect, filterLogsToArguments, getRandomInteger, + hexLeftPad, hexRandom, Numberish, randomAddress, @@ -18,14 +19,13 @@ import { TestEth2DaiBridgeContract, TestEth2DaiBridgeEvents, TestEth2DaiBridgeSellAllAmountEventArgs, + TestEth2DaiBridgeTokenApproveEventArgs, TestEth2DaiBridgeTokenTransferEventArgs, } from '../src'; -blockchainTests.resets('Eth2DaiBridge unit tests', env => { +blockchainTests.resets.only('Eth2DaiBridge unit tests', env => { const txHelper = new TransactionHelper(env.web3Wrapper, artifacts); let testContract: TestEth2DaiBridgeContract; - let daiTokenAddress: string; - let wethTokenAddress: string; before(async () => { testContract = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync( @@ -34,18 +34,6 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => { env.txDefaults, 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()', () => { @@ -57,109 +45,126 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => { }); describe('withdrawTo()', () => { - interface TransferOpts { - toTokenAddress: string; + interface WithdrawToOpts { + toTokenAddress?: string; + fromTokenAddress?: string; toAddress: string; amount: Numberish; fromTokenBalance: Numberish; revertReason: string; fillAmount: Numberish; + toTokentransferRevertReason: string; + toTokenTransferReturnData: string; } - function createTransferOpts(opts?: Partial): TransferOpts { + interface WithdrawToResult { + opts: WithdrawToOpts; + result: string; + logs: DecodedLogs; + } + + function createWithdrawToOpts(opts?: Partial): WithdrawToOpts { return { - toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0], toAddress: randomAddress(), amount: getRandomInteger(1, 100e18), revertReason: '', fillAmount: getRandomInteger(1, 100e18), fromTokenBalance: getRandomInteger(1, 100e18), + toTokentransferRevertReason: '', + toTokenTransferReturnData: hexLeftPad(1), ...opts, }; } - async function transferAsync(opts?: Partial): Promise<[string, DecodedLogs]> { - const _opts = createTransferOpts(opts); + async function withdrawToAsync(opts?: Partial): Promise { + const _opts = createWithdrawToOpts(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, + // Create tokens and balances. + if (_opts.fromTokenAddress === undefined) { + [_opts.fromTokenAddress] = await txHelper.getResultAndReceiptAsync( + testContract.createToken, + new BigNumber(_opts.fromTokenBalance), + ); + } + 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(). const [result, { logs }] = await txHelper.getResultAndReceiptAsync( testContract.withdrawTo, + // "to" token address _opts.toTokenAddress, + // Random from address. randomAddress(), + // To address. _opts.toAddress, 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]; - } - - function getOppositeToken(tokenAddress: string): string { - if (tokenAddress === daiTokenAddress) { - return wethTokenAddress; - } - return daiTokenAddress; + return { + opts: _opts, + result, + logs: (logs as any) as DecodedLogs, + }; } it('returns magic bytes on success', async () => { const BRIDGE_SUCCESS_RETURN_DATA = '0xdc1600f3'; - const [result] = await transferAsync(); + const { result } = await withdrawToAsync(); expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA); }); it('calls `Eth2Dai.sellAllAmount()`', async () => { - const opts = createTransferOpts(); - const [, logs] = await transferAsync(opts); + const { opts, logs } = await withdrawToAsync(); const transfers = filterLogsToArguments( logs, TestEth2DaiBridgeEvents.SellAllAmount, ); 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].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( + it('sets an unlimited allowance on the `fromTokenAddress` token', async () => { + const { opts, logs } = await withdrawToAsync(); + const approvals = filterLogsToArguments( logs, - TestEth2DaiBridgeEvents.SellAllAmount, + TestEth2DaiBridgeEvents.TokenApprove, ); - expect(transfers.length).to.eq(1); - expect(transfers[0].sellToken).to.eq(daiTokenAddress); - expect(transfers[0].buyToken).to.eq(wethTokenAddress); + expect(approvals.length).to.eq(1); + expect(approvals[0].token).to.eq(opts.fromTokenAddress); + 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 () => { - const opts = createTransferOpts({ toTokenAddress: daiTokenAddress }); - const [, logs] = await transferAsync(opts); - const transfers = filterLogsToArguments( + it('does not set an unlimited allowance on the `fromTokenAddress` token if already set', async () => { + const { opts } = await withdrawToAsync(); + const { logs } = await withdrawToAsync({ fromTokenAddress: opts.fromTokenAddress }); + const approvals = filterLogsToArguments( logs, - TestEth2DaiBridgeEvents.SellAllAmount, + TestEth2DaiBridgeEvents.TokenApprove, ); - expect(transfers.length).to.eq(1); - expect(transfers[0].sellToken).to.eq(wethTokenAddress); - expect(transfers[0].buyToken).to.eq(daiTokenAddress); + expect(approvals.length).to.eq(0); }); it('transfers filled amount to `to`', async () => { - const opts = createTransferOpts(); - const [, logs] = await transferAsync(opts); + const { opts, logs } = await withdrawToAsync(); const transfers = filterLogsToArguments( logs, TestEth2DaiBridgeEvents.TokenTransfer, @@ -172,9 +177,25 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => { }); it('fails if `Eth2Dai.sellAllAmount()` reverts', async () => { - const opts = createTransferOpts({ revertReason: 'FOOBAR' }); - const tx = transferAsync(opts); + const opts = createWithdrawToOpts({ revertReason: 'FOOBAR' }); + const tx = withdrawToAsync(opts); 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) }); + }); }); }); diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index ff6ab95a27..b3faf3e356 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -14,6 +14,7 @@ "generated-artifacts/IAuthorizable.json", "generated-artifacts/IERC20Bridge.json", "generated-artifacts/IEth2Dai.json", + "generated-artifacts/IWallet.json", "generated-artifacts/MixinAssetProxyDispatcher.json", "generated-artifacts/MixinAuthorizable.json", "generated-artifacts/MultiAssetProxy.json",