From f38514ed0ded8ec480a2ff4a18e044acf48b5606 Mon Sep 17 00:00:00 2001 From: xianny Date: Mon, 1 Jun 2020 12:35:39 -0700 Subject: [PATCH] implement UniswapV2 Bridge --- .../contracts/src/bridges/UniswapV2Bridge.sol | 141 +++++++++++ .../src/interfaces/IUniswapV2Router01.sol | 64 +++++ .../contracts/test/TestUniswapV2Bridge.sol | 231 ++++++++++++++++++ contracts/asset-proxy/package.json | 2 +- contracts/asset-proxy/src/artifacts.ts | 6 + contracts/asset-proxy/src/wrappers.ts | 3 + contracts/asset-proxy/test/artifacts.ts | 6 + .../asset-proxy/test/uniswapv2_bridge.ts | 140 +++++++++++ contracts/asset-proxy/test/wrappers.ts | 3 + contracts/asset-proxy/tsconfig.json | 8 +- .../contracts/src/ERC20BridgeSampler.sol | 99 ++++++++ .../contracts/src/IUniswapV2Pair.sol | 26 ++ contracts/erc20-bridge-sampler/package.json | 2 +- .../erc20-bridge-sampler/test/artifacts.ts | 2 + .../erc20-bridge-sampler/test/wrappers.ts | 1 + contracts/erc20-bridge-sampler/tsconfig.json | 1 + .../contracts/src/DeploymentConstants.sol | 12 + 17 files changed, 744 insertions(+), 3 deletions(-) create mode 100644 contracts/asset-proxy/contracts/src/bridges/UniswapV2Bridge.sol create mode 100644 contracts/asset-proxy/contracts/src/interfaces/IUniswapV2Router01.sol create mode 100644 contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol create mode 100644 contracts/asset-proxy/test/uniswapv2_bridge.ts create mode 100644 contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Pair.sol diff --git a/contracts/asset-proxy/contracts/src/bridges/UniswapV2Bridge.sol b/contracts/asset-proxy/contracts/src/bridges/UniswapV2Bridge.sol new file mode 100644 index 0000000000..4e3633be88 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapV2Bridge.sol @@ -0,0 +1,141 @@ +/* + + Copyright 2020 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-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; +import "@0x/contracts-utils/contracts/src/LibAddressArray.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "../interfaces/IUniswapV2Router01.sol"; +import "../interfaces/IERC20Bridge.sol"; + + +// solhint-disable space-after-comma +// solhint-disable not-rely-on-time +contract UniswapV2Bridge is + IERC20Bridge, + IWallet, + DeploymentConstants +{ + using LibAddressArray for address[]; + + struct TransferState { + address fromTokenAddress; + uint256 fromTokenBalance; + uint256 boughtAmount; + } + + /// @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 from The maker (this contract). + /// @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 bridgeTransferFrom( + address toTokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + // hold variables to get around stack depth limitations + TransferState memory state; + + // Decode the bridge data to get the `fromTokenAddress`. + (state.fromTokenAddress) = abi.decode(bridgeData, (address)); + + // Just transfer the tokens if they're the same. + if (state.fromTokenAddress == toTokenAddress) { // NOT TESTED + LibERC20Token.transfer(state.fromTokenAddress, to, amount); + return BRIDGE_SUCCESS; + } + + // Get our balance of `fromTokenAddress` token. + state.fromTokenBalance = IERC20Token(state.fromTokenAddress).balanceOf(address(this)); + + // // Grant the Uniswap router an allowance. // FIXME: REVERTING + // LibERC20Token.approve( + // state.fromTokenAddress, + // _getUniswapV2Router01Address(), + // // state.fromTokenBalance + // uint256(-1) + // ); + + // Convert directly from fromTokenAddress to toTokenAddress + address[] memory path = new address[](2); + path = path.append(state.fromTokenAddress); + path = path.append(toTokenAddress); + + // Buy as much `toTokenAddress` token with `fromTokenAddress` token + // and transfer it to `to`. + IUniswapV2Router01 router = IUniswapV2Router01(_getUniswapV2Router01Address()); + uint[] memory amounts = router.swapExactTokensForTokens( + // Sell all tokens we hold. + state.fromTokenBalance, + // Minimum buy amount. + amount, + // Convert `fromTokenAddress` to `toTokenAddress`. + path, + // Recipient is `to`. + to, + // Expires after this block. + block.timestamp + ); + + state.boughtAmount = amounts[1]; + + emit ERC20BridgeTransfer( + // input token + state.fromTokenAddress, + // output token + toTokenAddress, + // input token amount + state.fromTokenBalance, + // output token amount + state.boughtAmount, + from, + to + ); + + 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; + } +} diff --git a/contracts/asset-proxy/contracts/src/interfaces/IUniswapV2Router01.sol b/contracts/asset-proxy/contracts/src/interfaces/IUniswapV2Router01.sol new file mode 100644 index 0000000000..d2abb9c0a0 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,64 @@ +/* + + Copyright 2020 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 IUniswapV2Router01 { + + /// @dev Swaps an exact amount of input tokens for as many output tokens as possible, along the route determined by the path. + /// The first element of path is the input token, the last is the output token, and any intermediate elements represent + /// intermediate pairs to trade through (if, for example, a direct pair does not exist). + /// @param amountIn The amount of input tokens to send. + /// @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert. + /// @param path An array of token addresses. path.length must be >= 2. Pools for each consecutive pair of addresses must exist and have liquidity. + /// @param to Recipient of the output tokens. + /// @param deadline Unix timestamp after which the transaction will revert. + /// @return amounts The input token amount and all subsequent output token amounts. + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + // /// @dev Receive an exact amount of output tokens for as few input tokens as possible, along the route determined by the path. + // /// The first element of path is the input token, the last is the output token, and any intermediate elements represent + // /// intermediate pairs to trade through (if, for example, a direct pair does not exist). + // /// * msg.sender should have already given the router an allowance of at least amountInMax on the input token. + // /// @param amountOut The amount of output tokens to receive. + // /// @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts. + // /// @param path An array of token addresses. path.length must be >= 2. Pools for each consecutive pair of addresses must exist and have liquidity. + // /// @param to Recipient of the output tokens. + // /// @param deadline Unix timestamp after which the transaction will revert. + // /// @return amounts The input token amount and all subsequent output token amounts. + // function swapTokensForExactTokens( + // uint amountOut, + // uint amountInMax, + // address[] calldata path, + // address to, + // uint deadline + // ) external returns (uint[] memory amounts); + + // /// @dev Given some asset amount and reserves, returns an amount of the other asset representing equivalent value. + // /// @param amountA The amount of assetA. + // /// @param reserveA The reserves of assetA. Call `getReserves` on the UniswapV2Router01 contract for this value. + // /// @param reserveB The reserves of assetB. Call `getReserves` on the UniswapV2Router01 contract for this value. + // function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); +} diff --git a/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol b/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol new file mode 100644 index 0000000000..0b30418761 --- /dev/null +++ b/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol @@ -0,0 +1,231 @@ +/* + + 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 "@0x/contracts-utils/contracts/src/LibAddressArray.sol"; +import "../src/bridges/UniswapV2Bridge.sol"; +import "../src/interfaces/IUniswapV2Router01.sol"; + + +/// @dev A minimalist ERC20/WETH token. +contract TestToken { + + using LibSafeMath for uint256; + + mapping (address => uint256) public balances; + string private _nextRevertReason; + + event TokenTransfer( + address token, + address from, + address to, + uint256 amount + ); + + event TokenApprove( + address spender, + uint256 allowance + ); + + /// @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 emits a TokenTransfer event on the caller + function transfer(address to, uint256 amount) + external + returns (bool) + { + _revertIfReasonExists(); + emit TokenTransfer(msg.sender, msg.sender, to, amount); + return true; + } + + /// @dev Just emits a TokenApprove event on the caller + function approve(address spender, uint256 allowance) + external + returns (bool) + { + emit TokenApprove(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); + } + + function allowance(address, address) external view returns (uint256) { + return 0; + } + + /// @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 TestRouter is + IUniswapV2Router01 +{ + + event TokenToTokenTransferInput( + address exchange, + uint256 tokensSold, + uint256 minTokensBought, + uint256 deadline, + address recipient, + address toTokenAddress + ); + + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts) + { + amounts = new uint[](2); + amounts[0] = amountIn; + // amounts[1] = address(this).balance; + amounts[1] = amountOutMin; + + emit TokenToTokenTransferInput( + msg.sender, + // tokens sold + amountIn, + // tokens bought + amountOutMin, + // deadline + deadline, + // recipient + to, + // output token (toTokenAddress) + path[1] + ); + } + +} + + +/// @dev UniswapV2Bridge overridden to mock tokens and Uniswap router +contract TestUniswapV2Bridge is + UniswapV2Bridge +{ + + // Token address to TestToken instance. + mapping (address => TestToken) private _testTokens; + // TestRouter instance. + TestRouter private _testRouter; + + constructor() public { + _testRouter = new TestRouter(); + } + + /// @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 new token + /// @param tokenAddress The token address. If zero, one will be created. + function createToken( + address tokenAddress + ) + external + payable + returns (TestToken token) + { + token = TestToken(tokenAddress); + if (tokenAddress == address(0)) { + token = new TestToken(); + } + _testTokens[address(token)] = token; + + return token; + } + + function _getUniswapV2Router01Address() + internal + view + returns (address) + { + return address(_testRouter); + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index 1505037275..002047f2fe 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -38,7 +38,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "abis": "./test/generated-artifacts/@(ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json", + "abis": "./test/generated-artifacts/@(ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index a9a5ffacf2..768e514e28 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -28,6 +28,7 @@ import * as IGasToken from '../generated-artifacts/IGasToken.json'; import * as IKyberNetworkProxy from '../generated-artifacts/IKyberNetworkProxy.json'; import * as IUniswapExchange from '../generated-artifacts/IUniswapExchange.json'; import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json'; +import * as IUniswapV2Router01 from '../generated-artifacts/IUniswapV2Router01.json'; import * as KyberBridge from '../generated-artifacts/KyberBridge.json'; import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; @@ -43,7 +44,9 @@ import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.jso import * as TestKyberBridge from '../generated-artifacts/TestKyberBridge.json'; import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json'; import * as TestUniswapBridge from '../generated-artifacts/TestUniswapBridge.json'; +import * as TestUniswapV2Bridge from '../generated-artifacts/TestUniswapV2Bridge.json'; import * as UniswapBridge from '../generated-artifacts/UniswapBridge.json'; +import * as UniswapV2Bridge from '../generated-artifacts/UniswapV2Bridge.json'; export const artifacts = { MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, @@ -62,6 +65,7 @@ export const artifacts = { KyberBridge: KyberBridge as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, + UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, IAssetData: IAssetData as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, @@ -76,6 +80,7 @@ export const artifacts = { IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, + IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, TestChaiBridge: TestChaiBridge as ContractArtifact, TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, TestDydxBridge: TestDydxBridge as ContractArtifact, @@ -84,4 +89,5 @@ export const artifacts = { TestKyberBridge: TestKyberBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, TestUniswapBridge: TestUniswapBridge as ContractArtifact, + TestUniswapV2Bridge: TestUniswapV2Bridge as ContractArtifact, }; diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 2dfc37f824..1ad10f45db 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -26,6 +26,7 @@ export * from '../generated-wrappers/i_gas_token'; export * from '../generated-wrappers/i_kyber_network_proxy'; export * from '../generated-wrappers/i_uniswap_exchange'; export * from '../generated-wrappers/i_uniswap_exchange_factory'; +export * from '../generated-wrappers/i_uniswap_v2_router01'; export * from '../generated-wrappers/kyber_bridge'; export * from '../generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../generated-wrappers/mixin_authorizable'; @@ -41,4 +42,6 @@ export * from '../generated-wrappers/test_eth2_dai_bridge'; export * from '../generated-wrappers/test_kyber_bridge'; export * from '../generated-wrappers/test_static_call_target'; export * from '../generated-wrappers/test_uniswap_bridge'; +export * from '../generated-wrappers/test_uniswap_v2_bridge'; export * from '../generated-wrappers/uniswap_bridge'; +export * from '../generated-wrappers/uniswap_v2_bridge'; diff --git a/contracts/asset-proxy/test/artifacts.ts b/contracts/asset-proxy/test/artifacts.ts index 7f7b5a7c8d..6b57210d8a 100644 --- a/contracts/asset-proxy/test/artifacts.ts +++ b/contracts/asset-proxy/test/artifacts.ts @@ -28,6 +28,7 @@ import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; import * as IUniswapExchange from '../test/generated-artifacts/IUniswapExchange.json'; import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json'; +import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; import * as KyberBridge from '../test/generated-artifacts/KyberBridge.json'; import * as MixinAssetProxyDispatcher from '../test/generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAuthorizable from '../test/generated-artifacts/MixinAuthorizable.json'; @@ -43,7 +44,9 @@ import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridg import * as TestKyberBridge from '../test/generated-artifacts/TestKyberBridge.json'; import * as TestStaticCallTarget from '../test/generated-artifacts/TestStaticCallTarget.json'; import * as TestUniswapBridge from '../test/generated-artifacts/TestUniswapBridge.json'; +import * as TestUniswapV2Bridge from '../test/generated-artifacts/TestUniswapV2Bridge.json'; import * as UniswapBridge from '../test/generated-artifacts/UniswapBridge.json'; +import * as UniswapV2Bridge from '../test/generated-artifacts/UniswapV2Bridge.json'; export const artifacts = { MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, @@ -62,6 +65,7 @@ export const artifacts = { KyberBridge: KyberBridge as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, + UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, IAssetData: IAssetData as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, @@ -76,6 +80,7 @@ export const artifacts = { IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, + IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, TestChaiBridge: TestChaiBridge as ContractArtifact, TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, TestDydxBridge: TestDydxBridge as ContractArtifact, @@ -84,4 +89,5 @@ export const artifacts = { TestKyberBridge: TestKyberBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, TestUniswapBridge: TestUniswapBridge as ContractArtifact, + TestUniswapV2Bridge: TestUniswapV2Bridge as ContractArtifact, }; diff --git a/contracts/asset-proxy/test/uniswapv2_bridge.ts b/contracts/asset-proxy/test/uniswapv2_bridge.ts new file mode 100644 index 0000000000..03078559cd --- /dev/null +++ b/contracts/asset-proxy/test/uniswapv2_bridge.ts @@ -0,0 +1,140 @@ +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + getRandomInteger, + getRandomPortion, + randomAddress, +} from '@0x/contracts-test-utils'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber, hexUtils } from '@0x/utils'; +import { DecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; + +import { TestUniswapV2BridgeContract, UniswapV2BridgeERC20BridgeTransferEventArgs, UniswapV2BridgeEvents } from './wrappers'; + +blockchainTests.resets.only('UniswapV2 unit tests', env => { + const FROM_TOKEN_DECIMALS = 6; + const TO_TOKEN_DECIMALS = 18; + const FROM_TOKEN_BASE = new BigNumber(10).pow(FROM_TOKEN_DECIMALS); + const TO_TOKEN_BASE = new BigNumber(10).pow(TO_TOKEN_DECIMALS); + let testContract: TestUniswapV2BridgeContract; + + before(async () => { + testContract = await TestUniswapV2BridgeContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapV2Bridge, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('isValidSignature()', () => { + it('returns success bytes', async () => { + const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381'; + const result = await testContract + .isValidSignature(hexUtils.random(), hexUtils.random(_.random(0, 32))) + .callAsync(); + expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE); + }); + }); + + describe('bridgeTransferFrom()', () => { + + interface TransferFromOpts { + toTokenAddress: string; + fromTokenAddress: string; + toAddress: string; + // Amount to pass into `bridgeTransferFrom()` + amount: BigNumber; + // Amount to convert in `trade()`. + fillAmount: BigNumber; + // Token balance of the bridge. + fromTokenBalance: BigNumber; + } + + interface TransferFromResult { + opts: TransferFromOpts; + result: string; + logs: DecodedLogs; + } + + function createTransferFromOpts(opts?: Partial): TransferFromOpts { + const amount = getRandomInteger(1, TO_TOKEN_BASE.times(100)); + return { + fromTokenAddress: constants.NULL_ADDRESS, + toTokenAddress: constants.NULL_ADDRESS, + amount, + toAddress: randomAddress(), + fillAmount: getRandomPortion(amount), + fromTokenBalance: getRandomInteger(1, FROM_TOKEN_BASE.times(100)), + ...opts, + }; + + } + + async function withdrawToAsync(opts?: Partial): Promise { + const _opts = createTransferFromOpts(opts); + const callData = { value: new BigNumber(_opts.fillAmount) }; + // Create the "from" token and exchange. + const createFromTokenFn = testContract.createToken( + _opts.fromTokenAddress, + ); + _opts.fromTokenAddress = await createFromTokenFn.callAsync(callData); + await createFromTokenFn.awaitTransactionSuccessAsync(callData); + + // Create the "to" token and exchange. + const createToTokenFn = testContract.createToken( + _opts.toTokenAddress, + ); + _opts.toTokenAddress = await createToTokenFn.callAsync(callData); + await createToTokenFn.awaitTransactionSuccessAsync(callData); + + // Set the token balance for the token we're converting from. + await testContract.setTokenBalance(_opts.fromTokenAddress).awaitTransactionSuccessAsync({ + value: new BigNumber(_opts.fromTokenBalance), + }); + + // Call bridgeTransferFrom(). + const bridgeTransferFromFn = testContract.bridgeTransferFrom( + // Output token + _opts.toTokenAddress, + // Random maker address. + randomAddress(), + // Recipient address. + _opts.toAddress, + // Transfer amount. + _opts.amount, + // ABI-encode the input token address as the bridge data. + hexUtils.leftPad(_opts.fromTokenAddress), + ); + const result = await bridgeTransferFromFn.callAsync(); + const { logs } = await bridgeTransferFromFn.awaitTransactionSuccessAsync(); + return { + opts: _opts, + result, + logs: (logs as any) as DecodedLogs, + }; + } + + 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 { opts, result, logs } = await withdrawToAsync(); + expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id'); + const transfers = filterLogsToArguments(logs, UniswapV2BridgeEvents.ERC20BridgeTransfer); + expect(transfers.length).to.eq(1); + expect(transfers[0].inputToken).to.eq(opts.fromTokenAddress, 'input token address'); + expect(transfers[0].outputToken).to.eq(opts.toTokenAddress, 'output token address'); + expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address'); + expect(transfers[0].inputTokenAmount).to.bignumber.eq(opts.fromTokenBalance, 'input token amount'); + expect(transfers[0].outputTokenAmount).to.bignumber.eq(opts.amount, 'output token amount'); + }); + }); +}); diff --git a/contracts/asset-proxy/test/wrappers.ts b/contracts/asset-proxy/test/wrappers.ts index 7026456250..a53cb31833 100644 --- a/contracts/asset-proxy/test/wrappers.ts +++ b/contracts/asset-proxy/test/wrappers.ts @@ -26,6 +26,7 @@ export * from '../test/generated-wrappers/i_gas_token'; export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_uniswap_exchange'; export * from '../test/generated-wrappers/i_uniswap_exchange_factory'; +export * from '../test/generated-wrappers/i_uniswap_v2_router01'; export * from '../test/generated-wrappers/kyber_bridge'; export * from '../test/generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../test/generated-wrappers/mixin_authorizable'; @@ -41,4 +42,6 @@ export * from '../test/generated-wrappers/test_eth2_dai_bridge'; export * from '../test/generated-wrappers/test_kyber_bridge'; export * from '../test/generated-wrappers/test_static_call_target'; export * from '../test/generated-wrappers/test_uniswap_bridge'; +export * from '../test/generated-wrappers/test_uniswap_v2_bridge'; export * from '../test/generated-wrappers/uniswap_bridge'; +export * from '../test/generated-wrappers/uniswap_v2_bridge'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index bbdde8b013..cc10ff7b0c 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -26,6 +26,7 @@ "generated-artifacts/IKyberNetworkProxy.json", "generated-artifacts/IUniswapExchange.json", "generated-artifacts/IUniswapExchangeFactory.json", + "generated-artifacts/IUniswapV2Router01.json", "generated-artifacts/KyberBridge.json", "generated-artifacts/MixinAssetProxyDispatcher.json", "generated-artifacts/MixinAuthorizable.json", @@ -41,7 +42,9 @@ "generated-artifacts/TestKyberBridge.json", "generated-artifacts/TestStaticCallTarget.json", "generated-artifacts/TestUniswapBridge.json", + "generated-artifacts/TestUniswapV2Bridge.json", "generated-artifacts/UniswapBridge.json", + "generated-artifacts/UniswapV2Bridge.json", "test/generated-artifacts/ChaiBridge.json", "test/generated-artifacts/CurveBridge.json", "test/generated-artifacts/DexForwarderBridge.json", @@ -65,6 +68,7 @@ "test/generated-artifacts/IKyberNetworkProxy.json", "test/generated-artifacts/IUniswapExchange.json", "test/generated-artifacts/IUniswapExchangeFactory.json", + "test/generated-artifacts/IUniswapV2Router01.json", "test/generated-artifacts/KyberBridge.json", "test/generated-artifacts/MixinAssetProxyDispatcher.json", "test/generated-artifacts/MixinAuthorizable.json", @@ -80,7 +84,9 @@ "test/generated-artifacts/TestKyberBridge.json", "test/generated-artifacts/TestStaticCallTarget.json", "test/generated-artifacts/TestUniswapBridge.json", - "test/generated-artifacts/UniswapBridge.json" + "test/generated-artifacts/TestUniswapV2Bridge.json", + "test/generated-artifacts/UniswapBridge.json", + "test/generated-artifacts/UniswapV2Bridge.json" ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index f5216c3a41..220232be58 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -34,6 +34,7 @@ import "./IUniswapExchangeQuotes.sol"; import "./ICurve.sol"; import "./ILiquidityProvider.sol"; import "./ILiquidityProviderRegistry.sol"; +import "./IUniswapV2Pair.sol"; contract ERC20BridgeSampler is @@ -344,6 +345,83 @@ contract ERC20BridgeSampler is } } + /// @dev Sample sell quotes from Uniswap V2. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromUniswapV2( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + + // get reserves for the token pair + IUniswapV2Pair pair = _getUniswapV2Pair(takerToken, makerToken); + (bool didSucceed, bytes memory resultData) = + address(pair).staticcall.gas(UNISWAP_CALL_GAS)(abi.encodeWithSelector(pair.getReserves.selector)); + + if (didSucceed) { + (uint112 takerTokenReserves, uint112 makerTokenReserves, uint32 blockTimestamp) = abi.decode(resultData, (uint112, uint112, uint32)); + } + + // // pseudo-code: sample using reserves + // for (uint256 i = 0; i < numSamples; i++) { + // takerTokenAmount = takerTokenAmounts[i]; + // makerTokenAmounts[i] = uniswapRouter.quote(takerTokenAmount, takerTokenReserves, makerTokenReserves) + // } + } + + function sampleSellsFromUniswapV2ViaEth( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + } + + function sampleBuysFromUniswapV2( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + } + + function sampleBuysFromUniswapV2ViaEth( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + } + /// @dev Sample sell quotes from Uniswap. /// @param takerToken Address of the taker token (what to sell). /// @param makerToken Address of the maker token (what to buy). @@ -656,6 +734,27 @@ contract ERC20BridgeSampler is return LibERC20Token.decimals(tokenAddress); } + // from https://uniswap.org/docs/v2/technical-considerations/pair-addresses/#create2 + function _getUniswapV2Pair( + address takerToken, + address makerToken + ) + private + pure + returns (IUniswapV2Pair pair) + { + address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // fixme(xianny): change into a param + + address addr = address(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(takerToken, makerToken)), + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' + )))); + + pair = IUniswapV2Pair(addr); + } + /// @dev Gracefully calls a Uniswap pricing function. /// @param uniswapExchangeAddress Address of an `IUniswapExchangeQuotes` exchange. /// @param functionSelector Selector of the target function. diff --git a/contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Pair.sol b/contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Pair.sol new file mode 100644 index 0000000000..92ab931952 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Pair.sol @@ -0,0 +1,26 @@ +/* + + Copyright 2020 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 IUniswapV2Pair { + /// @dev Returns the reserves of token0 and token1 used to price trades and distribute liquidity. + /// Also returns the block.timestamp (mod 2**32) of the last block during which an interaction occured for the pair. + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); +} diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index 16a6893533..322f3e70fc 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|IUniswapV2Pair|TestERC20BridgeSampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index 3e595b577e..e5b85ec029 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -17,6 +17,7 @@ import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkPr import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json'; import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json'; import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; +import * as IUniswapV2Pair from '../test/generated-artifacts/IUniswapV2Pair.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; export const artifacts = { DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, @@ -31,5 +32,6 @@ export const artifacts = { ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, + IUniswapV2Pair: IUniswapV2Pair as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, }; diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index 94707cc529..8ba3e7d2f8 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -15,4 +15,5 @@ export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_registry'; export * from '../test/generated-wrappers/i_uniswap_exchange_quotes'; +export * from '../test/generated-wrappers/i_uniswap_v2_pair'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index 0000dc7c83..ab892f8c7e 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -21,6 +21,7 @@ "test/generated-artifacts/ILiquidityProvider.json", "test/generated-artifacts/ILiquidityProviderRegistry.json", "test/generated-artifacts/IUniswapExchangeQuotes.json", + "test/generated-artifacts/IUniswapV2Pair.json", "test/generated-artifacts/TestERC20BridgeSampler.json" ], "exclude": ["./deploy/solc/solc_bin"] diff --git a/contracts/utils/contracts/src/DeploymentConstants.sol b/contracts/utils/contracts/src/DeploymentConstants.sol index 30a6cd0fa9..73dc9c55b4 100644 --- a/contracts/utils/contracts/src/DeploymentConstants.sol +++ b/contracts/utils/contracts/src/DeploymentConstants.sol @@ -32,6 +32,10 @@ contract DeploymentConstants { address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; // /// @dev Kovan address of the `UniswapExchangeFactory` contract. // address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30; + /// @dev Mainnet address of the `UniswapV2Router01` contract. + address constant private UNISWAP_V2_ROUTER_01_ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; + // /// @dev Kovan address of the `UniswapV2Router01` contract. + // address constant private UNISWAP_V2_ROUTER_01_ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; /// @dev Mainnet address of the Eth2Dai `MatchingMarket` contract. address constant private ETH2DAI_ADDRESS = 0x794e6e91555438aFc3ccF1c5076A74F42133d08D; // /// @dev Kovan address of the Eth2Dai `MatchingMarket` contract. @@ -93,6 +97,14 @@ contract DeploymentConstants { return UNISWAP_EXCHANGE_FACTORY_ADDRESS; } + function _getUniswapV2Router01Address() + internal + view + returns (address uniswapV2Address) + { + return UNISWAP_V2_ROUTER_01_ADDRESS; + } + /// @dev An overridable way to retrieve the Eth2Dai `MatchingMarket` contract. /// @return eth2daiAddress The Eth2Dai `MatchingMarket` contract. function _getEth2DaiAddress()