@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:
parent
48f7a24505
commit
bb87c8e7b5
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
38
contracts/asset-proxy/contracts/src/interfaces/IWallet.sol
Normal file
38
contracts/asset-proxy/contracts/src/interfaces/IWallet.sol
Normal 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);
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
@ -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) });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user