Merge pull request #2233 from 0xProject/feat/erc20-bridge/uniswap
UniswapBridge
This commit is contained in:
@@ -25,6 +25,10 @@
|
||||
{
|
||||
"note": "Add `Eth2DaiBridge`",
|
||||
"pr": 2221
|
||||
},
|
||||
{
|
||||
"note": "Add `UniswapBridge`",
|
||||
"pr": 2233
|
||||
}
|
||||
],
|
||||
"timestamp": 1570135330
|
||||
|
@@ -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-libs/contracts/src/IWallet.sol";
|
||||
import "../interfaces/IERC20Bridge.sol";
|
||||
import "../interfaces/IEth2Dai.sol";
|
||||
import "../interfaces/IWallet.sol";
|
||||
|
||||
|
||||
// solhint-disable space-after-comma
|
||||
|
218
contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol
Normal file
218
contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
|
||||
import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol";
|
||||
import "../interfaces/IUniswapExchangeFactory.sol";
|
||||
import "../interfaces/IUniswapExchange.sol";
|
||||
import "../interfaces/IERC20Bridge.sol";
|
||||
|
||||
|
||||
// solhint-disable space-after-comma
|
||||
// solhint-disable not-rely-on-time
|
||||
contract UniswapBridge is
|
||||
IERC20Bridge,
|
||||
IWallet
|
||||
{
|
||||
/* Mainnet addresses */
|
||||
address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
|
||||
address constant private WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||
|
||||
// Struct to hold `withdrawTo()` local variables in memory and to avoid
|
||||
// stack overflows.
|
||||
struct WithdrawToState {
|
||||
IUniswapExchange exchange;
|
||||
uint256 fromTokenBalance;
|
||||
IEtherToken weth;
|
||||
}
|
||||
|
||||
// solhint-disable no-empty-blocks
|
||||
/// @dev Payable fallback to receive ETH from uniswap.
|
||||
function ()
|
||||
external
|
||||
payable
|
||||
{}
|
||||
|
||||
/// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of
|
||||
/// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress`
|
||||
/// token encoded in the bridge data.
|
||||
/// @param toTokenAddress The token to buy and transfer to `to`.
|
||||
/// @param to The recipient of the bought tokens.
|
||||
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
|
||||
/// @param bridgeData The abi-encoded "from" token address.
|
||||
/// @return success The magic bytes if successful.
|
||||
function withdrawTo(
|
||||
address toTokenAddress,
|
||||
address /* from */,
|
||||
address to,
|
||||
uint256 amount,
|
||||
bytes calldata bridgeData
|
||||
)
|
||||
external
|
||||
returns (bytes4 success)
|
||||
{
|
||||
// State memory object to avoid stack overflows.
|
||||
WithdrawToState memory state;
|
||||
// Decode the bridge data to get the `fromTokenAddress`.
|
||||
(address fromTokenAddress) = abi.decode(bridgeData, (address));
|
||||
|
||||
// Just transfer the tokens if they're the same.
|
||||
if (fromTokenAddress == toTokenAddress) {
|
||||
IERC20Token(fromTokenAddress).transfer(to, amount);
|
||||
return BRIDGE_SUCCESS;
|
||||
}
|
||||
|
||||
// Get the exchange for the token pair.
|
||||
state.exchange = _getUniswapExchangeForTokenPair(
|
||||
fromTokenAddress,
|
||||
toTokenAddress
|
||||
);
|
||||
// Get our balance of `fromTokenAddress` token.
|
||||
state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this));
|
||||
// Get the weth contract.
|
||||
state.weth = getWethContract();
|
||||
|
||||
// Convert from WETH to a token.
|
||||
if (fromTokenAddress == address(state.weth)) {
|
||||
// Unwrap the WETH.
|
||||
state.weth.withdraw(state.fromTokenBalance);
|
||||
// Buy as much of `toTokenAddress` token with ETH as possible and
|
||||
// transfer it to `to`.
|
||||
state.exchange.ethToTokenTransferInput.value(state.fromTokenBalance)(
|
||||
// Minimum buy amount.
|
||||
amount,
|
||||
// Expires after this block.
|
||||
block.timestamp,
|
||||
// Recipient is `to`.
|
||||
to
|
||||
);
|
||||
|
||||
// Convert from a token to WETH.
|
||||
} else if (toTokenAddress == address(state.weth)) {
|
||||
// Grant the exchange an allowance.
|
||||
_grantExchangeAllowance(state.exchange, fromTokenAddress);
|
||||
// Buy as much ETH with `fromTokenAddress` token as possible.
|
||||
uint256 ethBought = state.exchange.tokenToEthSwapInput(
|
||||
// Sell all tokens we hold.
|
||||
state.fromTokenBalance,
|
||||
// Minimum buy amount.
|
||||
amount,
|
||||
// Expires after this block.
|
||||
block.timestamp
|
||||
);
|
||||
// Wrap the ETH.
|
||||
state.weth.deposit.value(ethBought)();
|
||||
// Transfer the WETH to `to`.
|
||||
IEtherToken(toTokenAddress).transfer(to, ethBought);
|
||||
|
||||
// Convert from one token to another.
|
||||
} else {
|
||||
// Grant the exchange an allowance.
|
||||
_grantExchangeAllowance(state.exchange, fromTokenAddress);
|
||||
// Buy as much `toTokenAddress` token with `fromTokenAddress` token
|
||||
// and transfer it to `to`.
|
||||
state.exchange.tokenToTokenTransferInput(
|
||||
// Sell all tokens we hold.
|
||||
state.fromTokenBalance,
|
||||
// Minimum buy amount.
|
||||
amount,
|
||||
// No minimum intermediate ETH buy amount.
|
||||
0,
|
||||
// Expires after this block.
|
||||
block.timestamp,
|
||||
// Recipient is `to`.
|
||||
to,
|
||||
// Convert to `toTokenAddress`.
|
||||
toTokenAddress
|
||||
);
|
||||
}
|
||||
return BRIDGE_SUCCESS;
|
||||
}
|
||||
|
||||
/// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker
|
||||
/// and sign for itself in orders. Always succeeds.
|
||||
/// @return magicValue Success bytes, always.
|
||||
function isValidSignature(
|
||||
bytes32,
|
||||
bytes calldata
|
||||
)
|
||||
external
|
||||
view
|
||||
returns (bytes4 magicValue)
|
||||
{
|
||||
return LEGACY_WALLET_MAGIC_VALUE;
|
||||
}
|
||||
|
||||
/// @dev Overridable way to get the weth contract.
|
||||
/// @return token The WETH contract.
|
||||
function getWethContract()
|
||||
public
|
||||
view
|
||||
returns (IEtherToken token)
|
||||
{
|
||||
return IEtherToken(WETH_ADDRESS);
|
||||
}
|
||||
|
||||
/// @dev Overridable way to get the uniswap exchange factory contract.
|
||||
/// @return factory The exchange factory contract.
|
||||
function getUniswapExchangeFactoryContract()
|
||||
public
|
||||
view
|
||||
returns (IUniswapExchangeFactory factory)
|
||||
{
|
||||
return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS);
|
||||
}
|
||||
|
||||
/// @dev Grants an unlimited allowance to the exchange for its token
|
||||
/// on behalf of this contract.
|
||||
/// @param exchange The Uniswap token exchange.
|
||||
/// @param tokenAddress The token address for the exchange.
|
||||
function _grantExchangeAllowance(IUniswapExchange exchange, address tokenAddress)
|
||||
private
|
||||
{
|
||||
IERC20Token(tokenAddress).approve(address(exchange), uint256(-1));
|
||||
}
|
||||
|
||||
/// @dev Retrieves the uniswap exchange for a given token pair.
|
||||
/// In the case of a WETH-token exchange, this will be the non-WETH token.
|
||||
/// In th ecase of a token-token exchange, this will be the first token.
|
||||
/// @param fromTokenAddress The address of the token we are converting from.
|
||||
/// @param toTokenAddress The address of the token we are converting to.
|
||||
/// @return exchange The uniswap exchange.
|
||||
function _getUniswapExchangeForTokenPair(
|
||||
address fromTokenAddress,
|
||||
address toTokenAddress
|
||||
)
|
||||
private
|
||||
view
|
||||
returns (IUniswapExchange exchange)
|
||||
{
|
||||
address exchangeTokenAddress = fromTokenAddress;
|
||||
// Whichever isn't WETH is the exchange token.
|
||||
if (fromTokenAddress == address(getWethContract())) {
|
||||
exchangeTokenAddress = toTokenAddress;
|
||||
}
|
||||
exchange = getUniswapExchangeFactoryContract().getExchange(exchangeTokenAddress);
|
||||
require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN");
|
||||
return exchange;
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
|
||||
interface IUniswapExchange {
|
||||
|
||||
/// @dev Buys at least `minTokensBought` tokens with ETH and transfer them
|
||||
/// to `recipient`.
|
||||
/// @param minTokensBought The minimum number of tokens to buy.
|
||||
/// @param deadline Time when this order expires.
|
||||
/// @param recipient Who to transfer the tokens to.
|
||||
/// @return tokensBought Amount of tokens bought.
|
||||
function ethToTokenTransferInput(
|
||||
uint256 minTokensBought,
|
||||
uint256 deadline,
|
||||
address recipient
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (uint256 tokensBought);
|
||||
|
||||
/// @dev Buys at least `minEthBought` ETH with tokens.
|
||||
/// @param tokensSold Amount of tokens to sell.
|
||||
/// @param minEthBought The minimum amount of ETH to buy.
|
||||
/// @param deadline Time when this order expires.
|
||||
/// @return ethBought Amount of tokens bought.
|
||||
function tokenToEthSwapInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline
|
||||
)
|
||||
external
|
||||
returns (uint256 ethBought);
|
||||
|
||||
/// @dev Buys at least `minTokensBought` tokens with the exchange token
|
||||
/// and transfer them to `recipient`.
|
||||
/// @param minTokensBought The minimum number of tokens to buy.
|
||||
/// @param minEthBought The minimum amount of intermediate ETH to buy.
|
||||
/// @param deadline Time when this order expires.
|
||||
/// @param recipient Who to transfer the tokens to.
|
||||
/// @param toTokenAddress The token being bought.
|
||||
/// @return tokensBought Amount of tokens bought.
|
||||
function tokenToTokenTransferInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minTokensBought,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline,
|
||||
address recipient,
|
||||
address toTokenAddress
|
||||
)
|
||||
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);
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
import "./IUniswapExchange.sol";
|
||||
|
||||
|
||||
interface IUniswapExchangeFactory {
|
||||
|
||||
/// @dev Get the exchange for a token.
|
||||
/// @param tokenAddress The address of the token contract.
|
||||
function getExchange(address tokenAddress)
|
||||
external
|
||||
view
|
||||
returns (IUniswapExchange);
|
||||
}
|
432
contracts/asset-proxy/contracts/test/TestUniswapBridge.sol
Normal file
432
contracts/asset-proxy/contracts/test/TestUniswapBridge.sol
Normal file
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
|
||||
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
|
||||
import "../src/bridges/UniswapBridge.sol";
|
||||
import "../src/interfaces/IUniswapExchangeFactory.sol";
|
||||
import "../src/interfaces/IUniswapExchange.sol";
|
||||
|
||||
|
||||
// solhint-disable no-simple-event-func-name
|
||||
contract TestEventsRaiser {
|
||||
|
||||
event TokenTransfer(
|
||||
address token,
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
);
|
||||
|
||||
event TokenApprove(
|
||||
address spender,
|
||||
uint256 allowance
|
||||
);
|
||||
|
||||
event WethDeposit(
|
||||
uint256 amount
|
||||
);
|
||||
|
||||
event WethWithdraw(
|
||||
uint256 amount
|
||||
);
|
||||
|
||||
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,
|
||||
uint256 deadline,
|
||||
address recipient,
|
||||
address toTokenAddress
|
||||
);
|
||||
|
||||
function raiseEthToTokenTransferInput(
|
||||
uint256 minTokensBought,
|
||||
uint256 deadline,
|
||||
address recipient
|
||||
)
|
||||
external
|
||||
{
|
||||
emit EthToTokenTransferInput(
|
||||
msg.sender,
|
||||
minTokensBought,
|
||||
deadline,
|
||||
recipient
|
||||
);
|
||||
}
|
||||
|
||||
function raiseTokenToEthSwapInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline
|
||||
)
|
||||
external
|
||||
{
|
||||
emit TokenToEthSwapInput(
|
||||
msg.sender,
|
||||
tokensSold,
|
||||
minEthBought,
|
||||
deadline
|
||||
);
|
||||
}
|
||||
|
||||
function raiseTokenToTokenTransferInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minTokensBought,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline,
|
||||
address recipient,
|
||||
address toTokenAddress
|
||||
)
|
||||
external
|
||||
{
|
||||
emit TokenToTokenTransferInput(
|
||||
msg.sender,
|
||||
tokensSold,
|
||||
minTokensBought,
|
||||
minEthBought,
|
||||
deadline,
|
||||
recipient,
|
||||
toTokenAddress
|
||||
);
|
||||
}
|
||||
|
||||
function raiseTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
)
|
||||
external
|
||||
{
|
||||
emit TokenTransfer(
|
||||
msg.sender,
|
||||
from,
|
||||
to,
|
||||
amount
|
||||
);
|
||||
}
|
||||
|
||||
function raiseTokenApprove(address spender, uint256 allowance)
|
||||
external
|
||||
{
|
||||
emit TokenApprove(spender, allowance);
|
||||
}
|
||||
|
||||
function raiseWethDeposit(uint256 amount)
|
||||
external
|
||||
{
|
||||
emit WethDeposit(amount);
|
||||
}
|
||||
|
||||
function raiseWethWithdraw(uint256 amount)
|
||||
external
|
||||
{
|
||||
emit WethWithdraw(amount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @dev A minimalist ERC20/WETH token.
|
||||
contract TestToken {
|
||||
|
||||
using LibSafeMath for uint256;
|
||||
|
||||
mapping (address => uint256) public balances;
|
||||
string private _nextRevertReason;
|
||||
|
||||
/// @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);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @dev Just calls `raiseTokenApprove()` on the caller.
|
||||
function approve(address spender, uint256 allowance)
|
||||
external
|
||||
returns (bool)
|
||||
{
|
||||
TestEventsRaiser(msg.sender).raiseTokenApprove(spender, allowance);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @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);
|
||||
}
|
||||
|
||||
/// @dev `IWETH.withdraw()` that just reduces balances and calls
|
||||
/// `raiseWethWithdraw()` on the caller.
|
||||
function withdraw(uint256 amount)
|
||||
external
|
||||
{
|
||||
_revertIfReasonExists();
|
||||
balances[msg.sender] = balances[msg.sender].safeSub(amount);
|
||||
msg.sender.transfer(amount);
|
||||
TestEventsRaiser(msg.sender).raiseWethWithdraw(amount);
|
||||
}
|
||||
|
||||
/// @dev Retrieve the balance for `owner`.
|
||||
function balanceOf(address owner)
|
||||
external
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
return balances[owner];
|
||||
}
|
||||
|
||||
function _revertIfReasonExists()
|
||||
private
|
||||
view
|
||||
{
|
||||
if (bytes(_nextRevertReason).length != 0) {
|
||||
revert(_nextRevertReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
contract TestExchange is
|
||||
IUniswapExchange
|
||||
{
|
||||
address public tokenAddress;
|
||||
string private _nextRevertReason;
|
||||
|
||||
constructor(address _tokenAddress) public {
|
||||
tokenAddress = _tokenAddress;
|
||||
}
|
||||
|
||||
function setFillBehavior(
|
||||
string calldata revertReason
|
||||
)
|
||||
external
|
||||
payable
|
||||
{
|
||||
_nextRevertReason = revertReason;
|
||||
}
|
||||
|
||||
function ethToTokenTransferInput(
|
||||
uint256 minTokensBought,
|
||||
uint256 deadline,
|
||||
address recipient
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (uint256 tokensBought)
|
||||
{
|
||||
TestEventsRaiser(msg.sender).raiseEthToTokenTransferInput(
|
||||
minTokensBought,
|
||||
deadline,
|
||||
recipient
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
return address(this).balance;
|
||||
}
|
||||
|
||||
function tokenToEthSwapInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline
|
||||
)
|
||||
external
|
||||
returns (uint256 ethBought)
|
||||
{
|
||||
TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput(
|
||||
tokensSold,
|
||||
minEthBought,
|
||||
deadline
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
uint256 fillAmount = address(this).balance;
|
||||
msg.sender.transfer(fillAmount);
|
||||
return fillAmount;
|
||||
}
|
||||
|
||||
function tokenToTokenTransferInput(
|
||||
uint256 tokensSold,
|
||||
uint256 minTokensBought,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline,
|
||||
address recipient,
|
||||
address toTokenAddress
|
||||
)
|
||||
external
|
||||
returns (uint256 tokensBought)
|
||||
{
|
||||
TestEventsRaiser(msg.sender).raiseTokenToTokenTransferInput(
|
||||
tokensSold,
|
||||
minTokensBought,
|
||||
minEthBought,
|
||||
deadline,
|
||||
recipient,
|
||||
toTokenAddress
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
return address(this).balance;
|
||||
}
|
||||
|
||||
function toTokenAddress()
|
||||
external
|
||||
view
|
||||
returns (address _tokenAddress)
|
||||
{
|
||||
return tokenAddress;
|
||||
}
|
||||
|
||||
function _revertIfReasonExists()
|
||||
private
|
||||
view
|
||||
{
|
||||
if (bytes(_nextRevertReason).length != 0) {
|
||||
revert(_nextRevertReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @dev UniswapBridge overridden to mock tokens and implement IUniswapExchangeFactory.
|
||||
contract TestUniswapBridge is
|
||||
IUniswapExchangeFactory,
|
||||
TestEventsRaiser,
|
||||
UniswapBridge
|
||||
{
|
||||
TestToken public wethToken;
|
||||
// Token address to TestToken instance.
|
||||
mapping (address => TestToken) private _testTokens;
|
||||
// Token address to TestExchange instance.
|
||||
mapping (address => TestExchange) private _testExchanges;
|
||||
|
||||
constructor() public {
|
||||
wethToken = new TestToken();
|
||||
_testTokens[address(wethToken)] = wethToken;
|
||||
}
|
||||
|
||||
/// @dev Sets the balance of this contract for an existing token.
|
||||
/// The wei attached will be the balance.
|
||||
function setTokenBalance(address tokenAddress)
|
||||
external
|
||||
payable
|
||||
{
|
||||
TestToken token = _testTokens[tokenAddress];
|
||||
token.deposit.value(msg.value)();
|
||||
}
|
||||
|
||||
/// @dev Sets the revert reason for an existing token.
|
||||
function setTokenRevertReason(address tokenAddress, string calldata revertReason)
|
||||
external
|
||||
{
|
||||
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();
|
||||
}
|
||||
_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`
|
||||
function getExchange(address tokenAddress)
|
||||
external
|
||||
view
|
||||
returns (IUniswapExchange)
|
||||
{
|
||||
return IUniswapExchange(_testExchanges[tokenAddress]);
|
||||
}
|
||||
|
||||
// @dev Use `wethToken`.
|
||||
function getWethContract()
|
||||
public
|
||||
view
|
||||
returns (IEtherToken)
|
||||
{
|
||||
return IEtherToken(address(wethToken));
|
||||
}
|
||||
|
||||
// @dev This contract will double as the Uniswap contract.
|
||||
function getUniswapExchangeFactoryContract()
|
||||
public
|
||||
view
|
||||
returns (IUniswapExchangeFactory)
|
||||
{
|
||||
return IUniswapExchangeFactory(address(this));
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@
|
||||
"compile:truffle": "truffle compile"
|
||||
},
|
||||
"config": {
|
||||
"abis": "./generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IWallet|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestStaticCallTarget).json",
|
||||
"abis": "./generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IUniswapExchange|IUniswapExchangeFactory|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json",
|
||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
|
||||
},
|
||||
"repository": {
|
||||
|
@@ -16,7 +16,8 @@ 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 IUniswapExchange from '../generated-artifacts/IUniswapExchange.json';
|
||||
import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.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';
|
||||
@@ -25,6 +26,8 @@ import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json';
|
||||
import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json';
|
||||
import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json';
|
||||
import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json';
|
||||
import * as TestUniswapBridge from '../generated-artifacts/TestUniswapBridge.json';
|
||||
import * as UniswapBridge from '../generated-artifacts/UniswapBridge.json';
|
||||
export const artifacts = {
|
||||
MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact,
|
||||
MixinAuthorizable: MixinAuthorizable as ContractArtifact,
|
||||
@@ -36,14 +39,17 @@ export const artifacts = {
|
||||
MultiAssetProxy: MultiAssetProxy as ContractArtifact,
|
||||
StaticCallProxy: StaticCallProxy as ContractArtifact,
|
||||
Eth2DaiBridge: Eth2DaiBridge as ContractArtifact,
|
||||
UniswapBridge: UniswapBridge as ContractArtifact,
|
||||
IAssetData: IAssetData as ContractArtifact,
|
||||
IAssetProxy: IAssetProxy as ContractArtifact,
|
||||
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
|
||||
IAuthorizable: IAuthorizable as ContractArtifact,
|
||||
IERC20Bridge: IERC20Bridge as ContractArtifact,
|
||||
IEth2Dai: IEth2Dai as ContractArtifact,
|
||||
IWallet: IWallet as ContractArtifact,
|
||||
IUniswapExchange: IUniswapExchange as ContractArtifact,
|
||||
IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact,
|
||||
TestERC20Bridge: TestERC20Bridge as ContractArtifact,
|
||||
TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact,
|
||||
TestStaticCallTarget: TestStaticCallTarget as ContractArtifact,
|
||||
TestUniswapBridge: TestUniswapBridge as ContractArtifact,
|
||||
};
|
||||
|
@@ -14,7 +14,8 @@ 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/i_uniswap_exchange';
|
||||
export * from '../generated-wrappers/i_uniswap_exchange_factory';
|
||||
export * from '../generated-wrappers/mixin_asset_proxy_dispatcher';
|
||||
export * from '../generated-wrappers/mixin_authorizable';
|
||||
export * from '../generated-wrappers/multi_asset_proxy';
|
||||
@@ -23,3 +24,5 @@ export * from '../generated-wrappers/static_call_proxy';
|
||||
export * from '../generated-wrappers/test_erc20_bridge';
|
||||
export * from '../generated-wrappers/test_eth2_dai_bridge';
|
||||
export * from '../generated-wrappers/test_static_call_target';
|
||||
export * from '../generated-wrappers/test_uniswap_bridge';
|
||||
export * from '../generated-wrappers/uniswap_bridge';
|
||||
|
365
contracts/asset-proxy/test/uniswap_bridge.ts
Normal file
365
contracts/asset-proxy/test/uniswap_bridge.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
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';
|
||||
|
||||
import {
|
||||
artifacts,
|
||||
TestUniswapBridgeContract,
|
||||
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 => {
|
||||
const txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
|
||||
let testContract: TestUniswapBridgeContract;
|
||||
let wethTokenAddress: string;
|
||||
|
||||
before(async () => {
|
||||
testContract = await TestUniswapBridgeContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestUniswapBridge,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
wethTokenAddress = await testContract.wethToken.callAsync();
|
||||
});
|
||||
|
||||
describe('isValidSignature()', () => {
|
||||
it('returns success bytes', async () => {
|
||||
const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381';
|
||||
const result = await testContract.isValidSignature.callAsync(hexRandom(), hexRandom(_.random(0, 32)));
|
||||
expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdrawTo()', () => {
|
||||
interface WithdrawToOpts {
|
||||
fromTokenAddress: string;
|
||||
toTokenAddress: string;
|
||||
fromTokenBalance: Numberish;
|
||||
toAddress: string;
|
||||
amount: Numberish;
|
||||
exchangeRevertReason: string;
|
||||
exchangeFillAmount: Numberish;
|
||||
toTokenRevertReason: string;
|
||||
fromTokenRevertReason: string;
|
||||
}
|
||||
|
||||
function createWithdrawToOpts(opts?: Partial<WithdrawToOpts>): WithdrawToOpts {
|
||||
return {
|
||||
fromTokenAddress: constants.NULL_ADDRESS,
|
||||
toTokenAddress: constants.NULL_ADDRESS,
|
||||
fromTokenBalance: getRandomInteger(1, 1e18),
|
||||
toAddress: randomAddress(),
|
||||
amount: getRandomInteger(1, 1e18),
|
||||
exchangeRevertReason: '',
|
||||
exchangeFillAmount: getRandomInteger(1, 1e18),
|
||||
toTokenRevertReason: '',
|
||||
fromTokenRevertReason: '',
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
interface WithdrawToResult {
|
||||
opts: WithdrawToOpts;
|
||||
result: string;
|
||||
logs: DecodedLogs;
|
||||
blockTime: number;
|
||||
}
|
||||
|
||||
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 as any) as DecodedLogs,
|
||||
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 { result } = await withdrawToAsync();
|
||||
expect(result).to.eq(AssetProxyId.ERC20Bridge);
|
||||
});
|
||||
|
||||
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].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.amount);
|
||||
});
|
||||
|
||||
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(opts.amount);
|
||||
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 approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(approvals.length).to.eq(1);
|
||||
expect(approvals[0].spender).to.eq(exchangeAddress);
|
||||
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token on subsequent calls', async () => {
|
||||
const { opts } = await withdrawToAsync();
|
||||
const { logs } = await withdrawToAsync(opts);
|
||||
const approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(approvals.length).to.eq(1);
|
||||
expect(approvals[0].spender).to.eq(exchangeAddress);
|
||||
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
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(opts.amount);
|
||||
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('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 on subsequent calls', async () => {
|
||||
const { opts } = await withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const { logs } = await withdrawToAsync(opts);
|
||||
const approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(approvals.length).to.eq(1);
|
||||
expect(approvals[0].spender).to.eq(exchangeAddress);
|
||||
expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
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(opts.amount);
|
||||
expect(calls[0].args.deadline).to.bignumber.eq(blockTime);
|
||||
expect(calls[0].args.recipient).to.eq(opts.toAddress);
|
||||
});
|
||||
|
||||
it('does not set any allowance', async () => {
|
||||
const { logs } = await withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const approvals = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
expect(approvals).to.be.empty('');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -14,7 +14,8 @@
|
||||
"generated-artifacts/IAuthorizable.json",
|
||||
"generated-artifacts/IERC20Bridge.json",
|
||||
"generated-artifacts/IEth2Dai.json",
|
||||
"generated-artifacts/IWallet.json",
|
||||
"generated-artifacts/IUniswapExchange.json",
|
||||
"generated-artifacts/IUniswapExchangeFactory.json",
|
||||
"generated-artifacts/MixinAssetProxyDispatcher.json",
|
||||
"generated-artifacts/MixinAuthorizable.json",
|
||||
"generated-artifacts/MultiAssetProxy.json",
|
||||
@@ -22,7 +23,9 @@
|
||||
"generated-artifacts/StaticCallProxy.json",
|
||||
"generated-artifacts/TestERC20Bridge.json",
|
||||
"generated-artifacts/TestEth2DaiBridge.json",
|
||||
"generated-artifacts/TestStaticCallTarget.json"
|
||||
"generated-artifacts/TestStaticCallTarget.json",
|
||||
"generated-artifacts/TestUniswapBridge.json",
|
||||
"generated-artifacts/UniswapBridge.json"
|
||||
],
|
||||
"exclude": ["./deploy/solc/solc_bin"]
|
||||
}
|
||||
|
@@ -105,6 +105,10 @@
|
||||
{
|
||||
"note": "Update `IncompleteFillError` to take an `errorCode`, `expectedAssetFillAmount`, and `actualAssetFillAmount` fields.",
|
||||
"pr": 2075
|
||||
},
|
||||
{
|
||||
"note": "Move `IWallet.sol` from `asset-proxy` and `exchange` packages to here.",
|
||||
"pr": 2233
|
||||
}
|
||||
],
|
||||
"timestamp": 1570135330
|
||||
|
@@ -35,7 +35,7 @@
|
||||
"compile:truffle": "truffle compile"
|
||||
},
|
||||
"config": {
|
||||
"abis": "./generated-artifacts/@(LibEIP712ExchangeDomain|LibExchangeRichErrors|LibFillResults|LibMath|LibMathRichErrors|LibOrder|LibZeroExTransaction|TestLibEIP712ExchangeDomain|TestLibFillResults|TestLibMath|TestLibOrder|TestLibZeroExTransaction).json",
|
||||
"abis": "./generated-artifacts/@(IWallet|LibEIP712ExchangeDomain|LibExchangeRichErrors|LibFillResults|LibMath|LibMathRichErrors|LibOrder|LibZeroExTransaction|TestLibEIP712ExchangeDomain|TestLibFillResults|TestLibMath|TestLibOrder|TestLibZeroExTransaction).json",
|
||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
|
||||
},
|
||||
"repository": {
|
||||
|
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { ContractArtifact } from 'ethereum-types';
|
||||
|
||||
import * as IWallet from '../generated-artifacts/IWallet.json';
|
||||
import * as LibEIP712ExchangeDomain from '../generated-artifacts/LibEIP712ExchangeDomain.json';
|
||||
import * as LibExchangeRichErrors from '../generated-artifacts/LibExchangeRichErrors.json';
|
||||
import * as LibFillResults from '../generated-artifacts/LibFillResults.json';
|
||||
@@ -18,6 +19,7 @@ import * as TestLibMath from '../generated-artifacts/TestLibMath.json';
|
||||
import * as TestLibOrder from '../generated-artifacts/TestLibOrder.json';
|
||||
import * as TestLibZeroExTransaction from '../generated-artifacts/TestLibZeroExTransaction.json';
|
||||
export const artifacts = {
|
||||
IWallet: IWallet as ContractArtifact,
|
||||
LibEIP712ExchangeDomain: LibEIP712ExchangeDomain as ContractArtifact,
|
||||
LibExchangeRichErrors: LibExchangeRichErrors as ContractArtifact,
|
||||
LibFillResults: LibFillResults as ContractArtifact,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
|
||||
* -----------------------------------------------------------------------------
|
||||
*/
|
||||
export * from '../generated-wrappers/i_wallet';
|
||||
export * from '../generated-wrappers/lib_e_i_p712_exchange_domain';
|
||||
export * from '../generated-wrappers/lib_exchange_rich_errors';
|
||||
export * from '../generated-wrappers/lib_fill_results';
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
|
||||
"include": ["./src/**/*", "./test/**/*", "./scripts/**/*", "./generated-wrappers/**/*"],
|
||||
"files": [
|
||||
"generated-artifacts/IWallet.json",
|
||||
"generated-artifacts/LibEIP712ExchangeDomain.json",
|
||||
"generated-artifacts/LibExchangeRichErrors.json",
|
||||
"generated-artifacts/LibFillResults.json",
|
||||
|
@@ -40,6 +40,7 @@ const SEPARATOR = ',';
|
||||
contractsDir: argv.contractsDir,
|
||||
artifactsDir: argv.artifactsDir,
|
||||
contracts,
|
||||
isOfflineMode: process.env.SOLC_OFFLINE ? true : undefined,
|
||||
};
|
||||
const compiler = new Compiler(opts);
|
||||
if (argv.watch) {
|
||||
|
@@ -28,7 +28,10 @@ import {
|
||||
createDirIfDoesNotExistAsync,
|
||||
getContractArtifactIfExistsAsync,
|
||||
getDependencyNameToPackagePath,
|
||||
getSolcJSAsync,
|
||||
getSolcJSFromPath,
|
||||
getSolcJSReleasesAsync,
|
||||
getSolcJSVersionFromPath,
|
||||
getSourcesWithDependencies,
|
||||
getSourceTreeHash,
|
||||
makeContractPathsRelative,
|
||||
@@ -106,7 +109,10 @@ export class Compiler {
|
||||
: {};
|
||||
assert.doesConformToSchema('compiler.json', config, compilerOptionsSchema);
|
||||
this._contractsDir = path.resolve(passedOpts.contractsDir || config.contractsDir || DEFAULT_CONTRACTS_DIR);
|
||||
this._solcVersionIfExists = passedOpts.solcVersion || config.solcVersion;
|
||||
this._solcVersionIfExists =
|
||||
process.env.SOLCJS_PATH !== undefined
|
||||
? getSolcJSVersionFromPath(process.env.SOLCJS_PATH)
|
||||
: passedOpts.solcVersion || config.solcVersion;
|
||||
this._compilerSettings = {
|
||||
...DEFAULT_COMPILER_SETTINGS,
|
||||
...config.compilerSettings,
|
||||
@@ -292,7 +298,11 @@ export class Compiler {
|
||||
compilerOutput = await compileDockerAsync(solcVersion, input.standardInput);
|
||||
} else {
|
||||
fullSolcVersion = solcJSReleases[solcVersion];
|
||||
compilerOutput = await compileSolcJSAsync(solcVersion, input.standardInput, this._isOfflineMode);
|
||||
const solcInstance =
|
||||
process.env.SOLCJS_PATH !== undefined
|
||||
? getSolcJSFromPath(process.env.SOLCJS_PATH)
|
||||
: await getSolcJSAsync(solcVersion, this._isOfflineMode);
|
||||
compilerOutput = await compileSolcJSAsync(solcInstance, input.standardInput);
|
||||
}
|
||||
if (compilerOutput.errors !== undefined) {
|
||||
printCompilationErrorsAndWarnings(compilerOutput.errors);
|
||||
|
@@ -136,16 +136,14 @@ export async function getSolcJSReleasesAsync(isOfflineMode: boolean): Promise<Bi
|
||||
|
||||
/**
|
||||
* Compiles the contracts and prints errors/warnings
|
||||
* @param solcVersion Version of a solc compiler
|
||||
* @param solcInstance Instance of a solc compiler
|
||||
* @param standardInput Solidity standard JSON input
|
||||
* @param isOfflineMode Offline mode flag
|
||||
*/
|
||||
export async function compileSolcJSAsync(
|
||||
solcVersion: string,
|
||||
solcInstance: solc.SolcInstance,
|
||||
standardInput: solc.StandardInput,
|
||||
isOfflineMode: boolean,
|
||||
): Promise<solc.StandardOutput> {
|
||||
const solcInstance = await getSolcJSAsync(solcVersion, isOfflineMode);
|
||||
const standardInputStr = JSON.stringify(standardInput);
|
||||
const standardOutputStr = solcInstance.compileStandardWrapper(standardInputStr);
|
||||
const compiled: solc.StandardOutput = JSON.parse(standardOutputStr);
|
||||
@@ -364,6 +362,22 @@ export async function getSolcJSAsync(solcVersion: string, isOfflineMode: boolean
|
||||
return solcInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the solidity compiler instance from a module path.
|
||||
* @param path The path to the solc module.
|
||||
*/
|
||||
export function getSolcJSFromPath(modulePath: string): solc.SolcInstance {
|
||||
return require(modulePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the solidity compiler version from a module path.
|
||||
* @param path The path to the solc module.
|
||||
*/
|
||||
export function getSolcJSVersionFromPath(modulePath: string): string {
|
||||
return require(modulePath).version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Solidity compiler emits the bytecode without a 0x prefix for a hex. This function fixes it if bytecode is present.
|
||||
* @param compiledContract The standard JSON output section for a contract. Geth modified in place.
|
||||
|
Reference in New Issue
Block a user