Merge pull request #2233 from 0xProject/feat/erc20-bridge/uniswap

UniswapBridge
This commit is contained in:
Lawrence Forman
2019-10-08 10:31:47 +09:00
committed by GitHub
20 changed files with 1187 additions and 14 deletions

View File

@@ -25,6 +25,10 @@
{
"note": "Add `Eth2DaiBridge`",
"pr": 2221
},
{
"note": "Add `UniswapBridge`",
"pr": 2233
}
],
"timestamp": 1570135330

View File

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

View 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;
}
}

View File

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

View File

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

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

View File

@@ -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": {

View File

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

View File

@@ -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';

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

View File

@@ -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"]
}

View File

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

View File

@@ -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": {

View File

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

View File

@@ -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';

View File

@@ -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",

View File

@@ -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) {

View File

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

View File

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