diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 278f85db45..677a1d3884 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -23,7 +23,11 @@ "pr": 2525 }, { - "note": "Add Gas Token freeing to `DexForwaderBridge` contract.", + "note": "Add `UniswapV2Bridge` bridge contract.", + "pr": 2590 + }, + { + "note": "Add Gas Token freeing to `DexForwarderBridge` contract.", "pr": 2536 } ] 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..4a40546406 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapV2Bridge.sol @@ -0,0 +1,135 @@ +/* + + 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 +{ + struct TransferState { + address[] path; + uint256 fromTokenBalance; + } + + /// @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 path of token addresses. Last element must be toTokenAddress + /// @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`. + // solhint-disable indent + state.path = abi.decode(bridgeData, (address[])); + // solhint-enable indent + + require(state.path.length >= 2, "UniswapV2Bridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); + require(state.path[state.path.length - 1] == toTokenAddress, "UniswapV2Bridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"); + + // Just transfer the tokens if they're the same. + if (state.path[0] == toTokenAddress) { + LibERC20Token.transfer(state.path[0], to, amount); + return BRIDGE_SUCCESS; + } + + // Get our balance of `fromTokenAddress` token. + state.fromTokenBalance = IERC20Token(state.path[0]).balanceOf(address(this)); + + // Grant the Uniswap router an allowance. + LibERC20Token.approveIfBelow( + state.path[0], + _getUniswapV2Router01Address(), + state.fromTokenBalance + ); + + // 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`. + state.path, + // Recipient is `to`. + to, + // Expires after this block. + block.timestamp + ); + + emit ERC20BridgeTransfer( + // input token + state.path[0], + // output token + toTokenAddress, + // input token amount + state.fromTokenBalance, + // output token amount + amounts[amounts.length - 1], + 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..a50d77bba3 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,40 @@ +/* + + 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); +} diff --git a/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol b/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol new file mode 100644 index 0000000000..04126f22e0 --- /dev/null +++ b/contracts/asset-proxy/contracts/test/TestUniswapV2Bridge.sol @@ -0,0 +1,253 @@ +/* + + 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"; + + +contract TestEventsRaiser { + + event TokenTransfer( + address token, + address from, + address to, + uint256 amount + ); + + event TokenApprove( + address spender, + uint256 allowance + ); + + event SwapExactTokensForTokensInput( + uint amountIn, + uint amountOutMin, + address toTokenAddress, + address to, + uint deadline + ); + + 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 raiseSwapExactTokensForTokensInput( + uint amountIn, + uint amountOutMin, + address toTokenAddress, + address to, + uint deadline + ) external + { + emit SwapExactTokensForTokensInput( + amountIn, + amountOutMin, + toTokenAddress, + to, + deadline + ); + } +} + + +/// @dev A minimalist ERC20 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, uint256 balance) + external + payable + { + balances[owner] = balance; + } + + /// @dev Just emits a TokenTransfer event on the caller + function transfer(address to, uint256 amount) + external + returns (bool) + { + TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount); + return true; + } + + /// @dev Just emits a TokenApprove event on the caller + function approve(address spender, uint256 allowance) + external + returns (bool) + { + TestEventsRaiser(msg.sender).raiseTokenApprove(spender, allowance); + return true; + } + + 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]; + } +} + + +/// @dev Mock the UniswapV2Router01 contract +contract TestRouter is + IUniswapV2Router01 +{ + string private _nextRevertReason; + + /// @dev Set the revert reason for `swapExactTokensForTokens`. + function setRevertReason(string calldata reason) + external + { + _nextRevertReason = reason; + } + + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts) + { + _revertIfReasonExists(); + + amounts = new uint[](path.length); + amounts[0] = amountIn; + amounts[amounts.length - 1] = amountOutMin; + + TestEventsRaiser(msg.sender).raiseSwapExactTokensForTokensInput( + // tokens sold + amountIn, + // tokens bought + amountOutMin, + // output token (toTokenAddress) + path[path.length - 1], + // recipient + to, + // deadline + deadline + ); + } + + function _revertIfReasonExists() + private + view + { + if (bytes(_nextRevertReason).length != 0) { + revert(_nextRevertReason); + } + } + +} + + +/// @dev UniswapV2Bridge overridden to mock tokens and Uniswap router +contract TestUniswapV2Bridge is + UniswapV2Bridge, + TestEventsRaiser +{ + + // Token address to TestToken instance. + mapping (address => TestToken) private _testTokens; + // TestRouter instance. + TestRouter private _testRouter; + + constructor() public { + _testRouter = new TestRouter(); + } + + function setRouterRevertReason(string calldata revertReason) + external + { + _testRouter.setRevertReason(revertReason); + } + + /// @dev Sets the balance of this contract for an existing token. + /// The wei attached will be the balance. + function setTokenBalance(address tokenAddress, uint256 balance) + external + { + TestToken token = _testTokens[tokenAddress]; + token.setBalance(address(this), balance); + } + + /// @dev Create a new token + /// @param tokenAddress The token address. If zero, one will be created. + function createToken( + address tokenAddress + ) + external + returns (TestToken token) + { + token = TestToken(tokenAddress); + if (tokenAddress == address(0)) { + token = new TestToken(); + } + _testTokens[address(token)] = token; + + return token; + } + + function getRouterAddress() + external + view + returns (address) + { + return address(_testRouter); + } + + 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..6e72b6115b --- /dev/null +++ b/contracts/asset-proxy/test/uniswapv2_bridge.ts @@ -0,0 +1,216 @@ +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + getRandomInteger, + randomAddress, +} from '@0x/contracts-test-utils'; +import { AssetProxyId } from '@0x/types'; +import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; +import { DecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; + +import { + TestUniswapV2BridgeContract, + TestUniswapV2BridgeEvents as ContractEvents, + TestUniswapV2BridgeSwapExactTokensForTokensInputEventArgs as SwapExactTokensForTokensArgs, + TestUniswapV2BridgeTokenApproveEventArgs as TokenApproveArgs, + TestUniswapV2BridgeTokenTransferEventArgs as TokenTransferArgs, +} from './wrappers'; + +blockchainTests.resets('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 { + tokenAddressesPath: string[]; + toAddress: string; + // Amount to pass into `bridgeTransferFrom()` + amount: BigNumber; + // Token balance of the bridge. + fromTokenBalance: BigNumber; + // Router reverts with this reason + routerRevertReason: string; + } + + interface TransferFromResult { + opts: TransferFromOpts; + result: string; + logs: DecodedLogs; + blocktime: number; + } + + function createTransferFromOpts(opts?: Partial): TransferFromOpts { + const amount = getRandomInteger(1, TO_TOKEN_BASE.times(100)); + return { + tokenAddressesPath: Array(2).fill(constants.NULL_ADDRESS), + amount, + toAddress: randomAddress(), + fromTokenBalance: getRandomInteger(1, FROM_TOKEN_BASE.times(100)), + routerRevertReason: '', + ...opts, + }; + } + + const bridgeDataEncoder = AbiEncoder.create('(address[])'); + + async function transferFromAsync(opts?: Partial): Promise { + const _opts = createTransferFromOpts(opts); + + for (let i = 0; i < _opts.tokenAddressesPath.length; i++) { + const createFromTokenFn = testContract.createToken(_opts.tokenAddressesPath[i]); + _opts.tokenAddressesPath[i] = await createFromTokenFn.callAsync(); + await createFromTokenFn.awaitTransactionSuccessAsync(); + } + + // Set the token balance for the token we're converting from. + await testContract + .setTokenBalance(_opts.tokenAddressesPath[0], _opts.fromTokenBalance) + .awaitTransactionSuccessAsync(); + + // Set revert reason for the router. + await testContract.setRouterRevertReason(_opts.routerRevertReason).awaitTransactionSuccessAsync(); + + // Call bridgeTransferFrom(). + const bridgeTransferFromFn = testContract.bridgeTransferFrom( + // Output token + _opts.tokenAddressesPath[_opts.tokenAddressesPath.length - 1], + // Random maker address. + randomAddress(), + // Recipient address. + _opts.toAddress, + // Transfer amount. + _opts.amount, + // ABI-encode the input token address as the bridge data. // FIXME + bridgeDataEncoder.encode([_opts.tokenAddressesPath]), + ); + const result = await bridgeTransferFromFn.callAsync(); + const receipt = await bridgeTransferFromFn.awaitTransactionSuccessAsync(); + return { + opts: _opts, + result, + logs: (receipt.logs as any) as DecodedLogs, + blocktime: await env.web3Wrapper.getBlockTimestampAsync(receipt.blockNumber), + }; + } + + it('returns magic bytes on success', async () => { + const { result } = await transferFromAsync(); + expect(result).to.eq(AssetProxyId.ERC20Bridge); + }); + + it('performs transfer when both tokens are the same', async () => { + const createTokenFn = testContract.createToken(constants.NULL_ADDRESS); + const tokenAddress = await createTokenFn.callAsync(); + await createTokenFn.awaitTransactionSuccessAsync(); + + const { opts, result, logs } = await transferFromAsync({ + tokenAddressesPath: [tokenAddress, tokenAddress], + }); + expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id'); + const transfers = filterLogsToArguments(logs, ContractEvents.TokenTransfer); + + expect(transfers.length).to.eq(1); + expect(transfers[0].token).to.eq(tokenAddress, 'input token address'); + expect(transfers[0].from).to.eq(testContract.address); + expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address'); + expect(transfers[0].amount).to.bignumber.eq(opts.amount, 'amount'); + }); + + describe('token -> token', async () => { + it('calls UniswapV2Router01.swapExactTokensForTokens()', async () => { + const { opts, result, logs, blocktime } = await transferFromAsync(); + expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id'); + const transfers = filterLogsToArguments( + logs, + ContractEvents.SwapExactTokensForTokensInput, + ); + + expect(transfers.length).to.eq(1); + expect(transfers[0].toTokenAddress).to.eq( + opts.tokenAddressesPath[opts.tokenAddressesPath.length - 1], + 'output token address', + ); + expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address'); + expect(transfers[0].amountIn).to.bignumber.eq(opts.fromTokenBalance, 'input token amount'); + expect(transfers[0].amountOutMin).to.bignumber.eq(opts.amount, 'output token amount'); + expect(transfers[0].deadline).to.bignumber.eq(blocktime, 'deadline'); + }); + + it('sets allowance for "from" token', async () => { + const { logs } = await transferFromAsync(); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const routerAddress = await testContract.getRouterAddress().callAsync(); + expect(approvals.length).to.eq(1); + expect(approvals[0].spender).to.eq(routerAddress); + expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('sets allowance for "from" token on subsequent calls', async () => { + const { opts } = await transferFromAsync(); + const { logs } = await transferFromAsync(opts); + const approvals = filterLogsToArguments(logs, ContractEvents.TokenApprove); + const routerAddress = await testContract.getRouterAddress().callAsync(); + expect(approvals.length).to.eq(1); + expect(approvals[0].spender).to.eq(routerAddress); + expect(approvals[0].allowance).to.bignumber.eq(constants.MAX_UINT256); + }); + + it('fails if the router fails', async () => { + const revertReason = 'FOOBAR'; + const tx = transferFromAsync({ + routerRevertReason: revertReason, + }); + return expect(tx).to.eventually.be.rejectedWith(revertReason); + }); + }); + describe('token -> token -> token', async () => { + it('calls UniswapV2Router01.swapExactTokensForTokens()', async () => { + const { opts, result, logs, blocktime } = await transferFromAsync({ + tokenAddressesPath: Array(3).fill(constants.NULL_ADDRESS), + }); + expect(result).to.eq(AssetProxyId.ERC20Bridge, 'asset proxy id'); + const transfers = filterLogsToArguments( + logs, + ContractEvents.SwapExactTokensForTokensInput, + ); + + expect(transfers.length).to.eq(1); + expect(transfers[0].toTokenAddress).to.eq( + opts.tokenAddressesPath[opts.tokenAddressesPath.length - 1], + 'output token address', + ); + expect(transfers[0].to).to.eq(opts.toAddress, 'recipient address'); + expect(transfers[0].amountIn).to.bignumber.eq(opts.fromTokenBalance, 'input token amount'); + expect(transfers[0].amountOutMin).to.bignumber.eq(opts.amount, 'output token amount'); + expect(transfers[0].deadline).to.bignumber.eq(blocktime, 'deadline'); + }); + }); + }); +}); 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/utils/contracts/src/DeploymentConstants.sol b/contracts/utils/contracts/src/DeploymentConstants.sol index 9876792a04..537193d53a 100644 --- a/contracts/utils/contracts/src/DeploymentConstants.sol +++ b/contracts/utils/contracts/src/DeploymentConstants.sol @@ -35,7 +35,7 @@ contract DeploymentConstants { /// @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_01ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; + // 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. diff --git a/contracts/utils/contracts/src/v06/AuthorizableV06.sol b/contracts/utils/contracts/src/v06/AuthorizableV06.sol index 7626de8070..9ce14c0fa2 100644 --- a/contracts/utils/contracts/src/v06/AuthorizableV06.sol +++ b/contracts/utils/contracts/src/v06/AuthorizableV06.sol @@ -35,13 +35,13 @@ contract AuthorizableV06 is _; } - /// @dev Whether an address is authorized to call privileged functions. - /// @param 0 Address to query. - /// @return 0 Whether the address is authorized. + // @dev Whether an address is authorized to call privileged functions. + // @param 0 Address to query. + // @return 0 Whether the address is authorized. mapping (address => bool) public override authorized; - /// @dev Whether an address is authorized to call privileged functions. - /// @param 0 Index of authorized address. - /// @return 0 Authorized address. + // @dev Whether an address is authorized to call privileged functions. + // @param 0 Index of authorized address. + // @return 0 Authorized address. address[] public override authorities; /// @dev Initializes the `owner` address.