From 4bdaa483037f19f3d181584953480d97087a64d3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 18 Mar 2020 13:58:58 -0400 Subject: [PATCH] `@0x/contracts-asset-proxy`: Create `DexForwarderBridge` bridge contract. --- contracts/asset-proxy/CHANGELOG.json | 4 + .../src/bridges/DexForwarderBridge.sol | 210 ++++++++++ .../contracts/test/TestDexForwarderBridge.sol | 220 ++++++++++ contracts/asset-proxy/package.json | 3 +- contracts/asset-proxy/src/artifacts.ts | 4 + .../asset-proxy/src/dex_forwarder_bridge.ts | 27 ++ contracts/asset-proxy/src/index.ts | 1 + contracts/asset-proxy/src/wrappers.ts | 2 + contracts/asset-proxy/test/artifacts.ts | 4 + .../asset-proxy/test/dex_forwarder_bridge.ts | 385 ++++++++++++++++++ contracts/asset-proxy/test/wrappers.ts | 2 + contracts/asset-proxy/tsconfig.json | 4 + 12 files changed, 865 insertions(+), 1 deletion(-) create mode 100644 contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol create mode 100644 contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol create mode 100644 contracts/asset-proxy/src/dex_forwarder_bridge.ts create mode 100644 contracts/asset-proxy/test/dex_forwarder_bridge.ts diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 715ce2afc6..b05613fb2a 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Added `MixinGasToken` allowing Gas Tokens to be freed", "pr": 2523 + }, + { + "note": "Add `DexForwaderBridge` bridge contract.", + "pr": 2525 } ] }, diff --git a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol new file mode 100644 index 0000000000..b73712e6c0 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol @@ -0,0 +1,210 @@ +/* + + 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/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; +import "../interfaces/IERC20Bridge.sol"; + + +// solhint-disable space-after-comma, indent +contract DexForwarderBridge is + IERC20Bridge, + IWallet +{ + using LibSafeMath for uint256; + + /// @dev Data needed to reconstruct a bridge call. + struct BridgeCall { + address target; + uint256 inputTokenAmount; + uint256 outputTokenAmount; + bytes bridgeData; + } + + /// @dev Intermediate state variables used by `bridgeTransferFrom()`, in + /// struct form to get around stack limits. + struct TransferFromState { + address inputToken; + uint256 initialInputTokenBalance; + uint256 callInputTokenAmount; + uint256 callOutputTokenAmount; + uint256 totalInputTokenSold; + BridgeCall[] calls; + } + + event DexForwarderBridgeCallFailed( + address indexed target, + address inputToken, + address outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount + ); + + /// @dev Executes a series of calls, forwarding . + /// @param outputToken The token being bought. + /// @param to The recipient of the bought tokens. + /// @param bridgeData The abi-encoeded input token address. + /// @return success The magic bytes if successful. + function bridgeTransferFrom( + address outputToken, + address /* from */, + address to, + uint256 /* amount */, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + TransferFromState memory state; + ( + state.inputToken, + state.calls + ) = abi.decode(bridgeData, (address, BridgeCall[])); + + state.initialInputTokenBalance = IERC20Token(state.inputToken).balanceOf(address(this)); + + for (uint256 i = 0; i < state.calls.length; ++i) { + // Stop if the we've sold all our input tokens. + if (state.totalInputTokenSold >= state.initialInputTokenBalance) { + break; + } + + BridgeCall memory call = state.calls[i]; + // Compute token amounts. + state.callInputTokenAmount = LibSafeMath.min256( + call.inputTokenAmount, + state.initialInputTokenBalance.safeSub(state.totalInputTokenSold) + ); + state.callOutputTokenAmount = LibMath.getPartialAmountFloor( + state.callInputTokenAmount, + call.inputTokenAmount, + call.outputTokenAmount + ); + + (bool didSucceed, ) = address(this) + .call(abi.encodeWithSelector( + this.executeBridgeCall.selector, + call.target, + to, + state.inputToken, + outputToken, + state.callInputTokenAmount, + state.callOutputTokenAmount, + call.bridgeData + )); + + if (!didSucceed) { + // Log errors. + emit DexForwarderBridgeCallFailed( + call.target, + state.inputToken, + outputToken, + state.callInputTokenAmount, + state.callOutputTokenAmount + ); + } else { + // Increase the amount of tokens sold. + state.totalInputTokenSold = state.totalInputTokenSold.safeAdd( + state.callInputTokenAmount + ); + } + } + // Revert if we were not able to sell our entire input token balance. + require( + state.totalInputTokenSold >= state.initialInputTokenBalance, + "DexForwaderBridge/INCOMPLETE_FILL" + ); + // Always succeed. + return BRIDGE_SUCCESS; + } + + /// @dev Transfers `inputToken` token to a bridge contract then calls + /// its `bridgeTransferFrom()`. This is executed in separate context + /// so we can revert the transfer on error. This can only be called + // by this contract itself. + /// @param bridge The bridge contract. + /// @param to The recipient of `outputToken` tokens. + /// @param inputToken The input token. + /// @param outputToken The output token. + /// @param inputTokenAmount The amount of input tokens to transfer to `bridge`. + /// @param outputTokenAmount The amount of expected output tokens to be sent + /// to `to` by `bridge`. + function executeBridgeCall( + address bridge, + address to, + address inputToken, + address outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount, + bytes calldata bridgeData + ) + external + { + // Must be called through `bridgeTransferFrom()`. + require(msg.sender == address(this), "DexForwaderBridge/ONLY_SELF"); + // `bridge` must not be this contract. + require(bridge != address(this), "DexForwaderBridge/ILLEGAL_BRIDGE"); + + // Get the starting balance of output tokens for `to`. + uint256 initialRecipientBalance = IERC20Token(outputToken).balanceOf(to); + + // Transfer input tokens to the bridge. + LibERC20Token.transfer(inputToken, bridge, inputTokenAmount); + + // Call the bridge. + (bool didSucceed, bytes memory resultData) = + bridge.call(abi.encodeWithSelector( + IERC20Bridge(0).bridgeTransferFrom.selector, + outputToken, + bridge, + to, + outputTokenAmount, + bridgeData + )); + + // Revert if the call failed or not enough tokens were bought. + // This will also undo the token transfer. + require( + didSucceed + && resultData.length == 32 + && LibBytes.readBytes32(resultData, 0) == bytes32(BRIDGE_SUCCESS) + && IERC20Token(outputToken).balanceOf(to).safeSub(initialRecipientBalance) >= outputTokenAmount + ); + } + + /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker + /// and sign for itself in orders. Always succeeds. + /// @return magicValue Magic 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/test/TestDexForwarderBridge.sol b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol new file mode 100644 index 0000000000..c0ada6c30b --- /dev/null +++ b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol @@ -0,0 +1,220 @@ +/* + + 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 "../src/bridges/DexForwarderBridge.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; + + +interface ITestDexForwarderBridge { + event BridgeTransferFromCalled( + address caller, + uint256 inputTokenBalance, + address inputToken, + address outputToken, + address from, + address to, + uint256 amount + ); + + event TokenTransferCalled( + address from, + address to, + uint256 amount + ); + + function emitBridgeTransferFromCalled( + address caller, + uint256 inputTokenBalance, + address inputToken, + address outputToken, + address from, + address to, + uint256 amount + ) external; + + function emitTokenTransferCalled( + address from, + address to, + uint256 amount + ) external; +} + + +interface ITestDexForwarderBridgeTestToken { + + function transfer(address to, uint256 amount) + external + returns (bool); + + function mint(address to, uint256 amount) + external; + + function balanceOf(address owner) external view returns (uint256); +} + + +contract TestDexForwarderBridgeTestBridge { + + bytes4 private _returnCode; + string private _revertError; + uint256 private _transferAmount; + ITestDexForwarderBridge private _testContract; + + constructor(bytes4 returnCode, string memory revertError) public { + _testContract = ITestDexForwarderBridge(msg.sender); + _returnCode = returnCode; + _revertError = revertError; + } + + function setTransferAmount(uint256 amount) external { + _transferAmount = amount; + } + + function bridgeTransferFrom( + address outputToken, + address from, + address to, + uint256 amount, + bytes memory bridgeData + ) + public + returns (bytes4 success) + { + if (bytes(_revertError).length != 0) { + revert(_revertError); + } + address inputToken = abi.decode(bridgeData, (address)); + _testContract.emitBridgeTransferFromCalled( + msg.sender, + ITestDexForwarderBridgeTestToken(inputToken).balanceOf(address(this)), + inputToken, + outputToken, + from, + to, + amount + ); + ITestDexForwarderBridgeTestToken(outputToken).mint(to, _transferAmount); + return _returnCode; + } +} + + +contract TestDexForwarderBridgeTestToken { + + using LibSafeMath for uint256; + + mapping(address => uint256) public balanceOf; + ITestDexForwarderBridge private _testContract; + + constructor() public { + _testContract = ITestDexForwarderBridge(msg.sender); + } + + function transfer(address to, uint256 amount) + external + returns (bool) + { + balanceOf[msg.sender] = balanceOf[msg.sender].safeSub(amount); + balanceOf[to] = balanceOf[to].safeAdd(amount); + _testContract.emitTokenTransferCalled(msg.sender, to, amount); + return true; + } + + function mint(address owner, uint256 amount) + external + { + balanceOf[owner] = balanceOf[owner].safeAdd(amount); + } + + function setBalance(address owner, uint256 amount) + external + { + balanceOf[owner] = amount; + } +} + + +contract TestDexForwarderBridge is + ITestDexForwarderBridge, + DexForwarderBridge +{ + function createBridge( + bytes4 returnCode, + string memory revertError + ) + public + returns (address bridge) + { + return address(new TestDexForwarderBridgeTestBridge(returnCode, revertError)); + } + + function createToken() public returns (address token) { + return address(new TestDexForwarderBridgeTestToken()); + } + + function setTokenBalance(address token, address owner, uint256 amount) public { + TestDexForwarderBridgeTestToken(token).setBalance(owner, amount); + } + + function setBridgeTransferAmount(address bridge, uint256 amount) public { + TestDexForwarderBridgeTestBridge(bridge).setTransferAmount(amount); + } + + function emitBridgeTransferFromCalled( + address caller, + uint256 inputTokenBalance, + address inputToken, + address outputToken, + address from, + address to, + uint256 amount + ) + public + { + emit BridgeTransferFromCalled( + caller, + inputTokenBalance, + inputToken, + outputToken, + from, + to, + amount + ); + } + + function emitTokenTransferCalled( + address from, + address to, + uint256 amount + ) + public + { + emit TokenTransferCalled( + from, + to, + amount + ); + } + + function balanceOf(address token, address owner) public view returns (uint256) { + return TestDexForwarderBridgeTestToken(token).balanceOf(owner); + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index 4d0845e253..1505037275 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|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|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|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { @@ -52,6 +52,7 @@ "homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md", "devDependencies": { "@0x/abi-gen": "^5.2.2", + "@0x/contract-wrappers": "^13.6.3", "@0x/contracts-gen": "^2.0.8", "@0x/contracts-test-utils": "^5.3.2", "@0x/contracts-utils": "^4.4.3", diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index b81c944ded..a9a5ffacf2 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json'; import * as CurveBridge from '../generated-artifacts/CurveBridge.json'; +import * as DexForwarderBridge from '../generated-artifacts/DexForwarderBridge.json'; import * as DydxBridge from '../generated-artifacts/DydxBridge.json'; import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json'; import * as ERC20BridgeProxy from '../generated-artifacts/ERC20BridgeProxy.json'; @@ -35,6 +36,7 @@ import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as Ownable from '../generated-artifacts/Ownable.json'; import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as TestChaiBridge from '../generated-artifacts/TestChaiBridge.json'; +import * as TestDexForwarderBridge from '../generated-artifacts/TestDexForwarderBridge.json'; import * as TestDydxBridge from '../generated-artifacts/TestDydxBridge.json'; import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json'; import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json'; @@ -54,6 +56,7 @@ export const artifacts = { StaticCallProxy: StaticCallProxy as ContractArtifact, ChaiBridge: ChaiBridge as ContractArtifact, CurveBridge: CurveBridge as ContractArtifact, + DexForwarderBridge: DexForwarderBridge as ContractArtifact, DydxBridge: DydxBridge as ContractArtifact, Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, KyberBridge: KyberBridge as ContractArtifact, @@ -74,6 +77,7 @@ export const artifacts = { IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, TestChaiBridge: TestChaiBridge as ContractArtifact, + TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, TestDydxBridge: TestDydxBridge as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, diff --git a/contracts/asset-proxy/src/dex_forwarder_bridge.ts b/contracts/asset-proxy/src/dex_forwarder_bridge.ts new file mode 100644 index 0000000000..77c76b06b9 --- /dev/null +++ b/contracts/asset-proxy/src/dex_forwarder_bridge.ts @@ -0,0 +1,27 @@ +import { AbiEncoder, BigNumber } from '@0x/utils'; + +export interface DexForwaderBridgeCall { + target: string; + inputTokenAmount: BigNumber; + outputTokenAmount: BigNumber; + bridgeData: string; +} + +export interface DexForwaderBridgeData { + inputToken: string; + calls: DexForwaderBridgeCall[]; +} + +export const dexForwarderBridgeDataEncoder = AbiEncoder.create([ + { name: 'inputToken', type: 'address' }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'target', type: 'address' }, + { name: 'inputTokenAmount', type: 'uint256' }, + { name: 'outputTokenAmount', type: 'uint256' }, + { name: 'bridgeData', type: 'bytes' }, + ], + }, +]); diff --git a/contracts/asset-proxy/src/index.ts b/contracts/asset-proxy/src/index.ts index 89d3053fa6..5f36e5e26b 100644 --- a/contracts/asset-proxy/src/index.ts +++ b/contracts/asset-proxy/src/index.ts @@ -88,3 +88,4 @@ export { } from './asset_data'; export * from './dydx_bridge_encoder'; +export * from './dex_forwarder_bridge'; diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 58f58445f9..2dfc37f824 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -5,6 +5,7 @@ */ export * from '../generated-wrappers/chai_bridge'; export * from '../generated-wrappers/curve_bridge'; +export * from '../generated-wrappers/dex_forwarder_bridge'; export * from '../generated-wrappers/dydx_bridge'; export * from '../generated-wrappers/erc1155_proxy'; export * from '../generated-wrappers/erc20_bridge_proxy'; @@ -33,6 +34,7 @@ export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/ownable'; export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/test_chai_bridge'; +export * from '../generated-wrappers/test_dex_forwarder_bridge'; export * from '../generated-wrappers/test_dydx_bridge'; export * from '../generated-wrappers/test_erc20_bridge'; export * from '../generated-wrappers/test_eth2_dai_bridge'; diff --git a/contracts/asset-proxy/test/artifacts.ts b/contracts/asset-proxy/test/artifacts.ts index 01937f8c5c..7f7b5a7c8d 100644 --- a/contracts/asset-proxy/test/artifacts.ts +++ b/contracts/asset-proxy/test/artifacts.ts @@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json'; import * as CurveBridge from '../test/generated-artifacts/CurveBridge.json'; +import * as DexForwarderBridge from '../test/generated-artifacts/DexForwarderBridge.json'; import * as DydxBridge from '../test/generated-artifacts/DydxBridge.json'; import * as ERC1155Proxy from '../test/generated-artifacts/ERC1155Proxy.json'; import * as ERC20BridgeProxy from '../test/generated-artifacts/ERC20BridgeProxy.json'; @@ -35,6 +36,7 @@ import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.js import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; import * as TestChaiBridge from '../test/generated-artifacts/TestChaiBridge.json'; +import * as TestDexForwarderBridge from '../test/generated-artifacts/TestDexForwarderBridge.json'; import * as TestDydxBridge from '../test/generated-artifacts/TestDydxBridge.json'; import * as TestERC20Bridge from '../test/generated-artifacts/TestERC20Bridge.json'; import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; @@ -54,6 +56,7 @@ export const artifacts = { StaticCallProxy: StaticCallProxy as ContractArtifact, ChaiBridge: ChaiBridge as ContractArtifact, CurveBridge: CurveBridge as ContractArtifact, + DexForwarderBridge: DexForwarderBridge as ContractArtifact, DydxBridge: DydxBridge as ContractArtifact, Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, KyberBridge: KyberBridge as ContractArtifact, @@ -74,6 +77,7 @@ export const artifacts = { IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, TestChaiBridge: TestChaiBridge as ContractArtifact, + TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, TestDydxBridge: TestDydxBridge as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, diff --git a/contracts/asset-proxy/test/dex_forwarder_bridge.ts b/contracts/asset-proxy/test/dex_forwarder_bridge.ts new file mode 100644 index 0000000000..96a7b770df --- /dev/null +++ b/contracts/asset-proxy/test/dex_forwarder_bridge.ts @@ -0,0 +1,385 @@ +import { ContractTxFunctionObj } from '@0x/contract-wrappers'; +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + getRandomInteger, + randomAddress, + shortZip, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils } from '@0x/utils'; +import { DecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DexForwaderBridgeCall, dexForwarderBridgeDataEncoder } from '../src/dex_forwarder_bridge'; + +import { artifacts } from './artifacts'; +import { + DexForwarderBridgeEvents, + TestDexForwarderBridgeBridgeTransferFromCalledEventArgs as BtfCalledEventArgs, + TestDexForwarderBridgeContract, + TestDexForwarderBridgeEvents as TestEvents, +} from './wrappers'; + +const { ZERO_AMOUNT } = constants; + +blockchainTests.resets('DexForwaderBridge unit tests', env => { + let testContract: TestDexForwarderBridgeContract; + let inputToken: string; + let outputToken: string; + const BRIDGE_SUCCESS = '0xdc1600f3'; + const BRIDGE_FAILURE = '0xffffffff'; + const BRIDGE_REVERT_ERROR = 'oopsie'; + const INCOMPLETE_FILL_REVERT = 'DexForwaderBridge/INCOMPLETE_FILL'; + const DEFAULTS = { + toAddress: randomAddress(), + }; + + before(async () => { + testContract = await TestDexForwarderBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestDexForwarderBridge, + env.provider, + env.txDefaults, + artifacts, + ); + // Create test tokens. + [inputToken, outputToken] = [ + await callAndTransactAsync(testContract.createToken()), + await callAndTransactAsync(testContract.createToken()), + ]; + }); + + async function callAndTransactAsync(fnCall: ContractTxFunctionObj): Promise { + const result = await fnCall.callAsync(); + await fnCall.awaitTransactionSuccessAsync({}, { shouldValidate: false }); + return result; + } + + function getRandomBridgeCall( + bridgeAddress: string, + fields: Partial = {}, + ): DexForwaderBridgeCall { + return { + target: bridgeAddress, + inputTokenAmount: getRandomInteger(1, '100e18'), + outputTokenAmount: getRandomInteger(1, '100e18'), + bridgeData: hexUtils.leftPad(inputToken), + ...fields, + }; + } + + describe('bridgeTransferFrom()', () => { + let goodBridgeCalls: DexForwaderBridgeCall[]; + let revertingBridgeCall: DexForwaderBridgeCall; + let failingBridgeCall: DexForwaderBridgeCall; + let allBridgeCalls: DexForwaderBridgeCall[]; + let totalFillableOutputAmount: BigNumber; + let totalFillableInputAmount: BigNumber; + let recipientOutputBalance: BigNumber; + + beforeEach(async () => { + goodBridgeCalls = []; + for (let i = 0; i < 4; ++i) { + goodBridgeCalls.push(await createBridgeCallAsync({ returnCode: BRIDGE_SUCCESS })); + } + revertingBridgeCall = await createBridgeCallAsync({ revertError: BRIDGE_REVERT_ERROR }); + failingBridgeCall = await createBridgeCallAsync({ returnCode: BRIDGE_FAILURE }); + allBridgeCalls = _.shuffle([failingBridgeCall, revertingBridgeCall, ...goodBridgeCalls]); + + totalFillableInputAmount = BigNumber.sum(...goodBridgeCalls.map(c => c.inputTokenAmount)); + totalFillableOutputAmount = BigNumber.sum(...goodBridgeCalls.map(c => c.outputTokenAmount)); + + // Grant the taker some output tokens. + await testContract.setTokenBalance( + outputToken, + DEFAULTS.toAddress, + (recipientOutputBalance = getRandomInteger(1, '100e18')), + ); + }); + + async function setForwarderInputBalanceAsync(amount: BigNumber): Promise { + await testContract + .setTokenBalance(inputToken, testContract.address, amount) + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); + } + + async function createBridgeCallAsync( + opts: Partial<{ + returnCode: string; + revertError: string; + callFields: Partial; + outputFillAmount: BigNumber; + }>, + ): Promise { + const { returnCode, revertError, callFields, outputFillAmount } = { + returnCode: BRIDGE_SUCCESS, + revertError: '', + ...opts, + }; + const bridge = await callAndTransactAsync(testContract.createBridge(returnCode, revertError)); + const call = getRandomBridgeCall(bridge, callFields); + await testContract + .setBridgeTransferAmount(call.target, outputFillAmount || call.outputTokenAmount) + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); + return call; + } + + async function callBridgeTransferFromAsync(opts: { + bridgeData: string; + sellAmount?: BigNumber; + buyAmount?: BigNumber; + }): Promise { + // Fund the forwarder with input tokens to sell. + await setForwarderInputBalanceAsync(opts.sellAmount || totalFillableInputAmount); + const call = testContract.bridgeTransferFrom( + outputToken, + testContract.address, + DEFAULTS.toAddress, + opts.buyAmount || totalFillableOutputAmount, + opts.bridgeData, + ); + const returnCode = await call.callAsync(); + if (returnCode !== BRIDGE_SUCCESS) { + throw new Error('Expected BRIDGE_SUCCESS'); + } + const receipt = await call.awaitTransactionSuccessAsync({}, { shouldValidate: false }); + // tslint:disable-next-line: no-unnecessary-type-assertion + return receipt.logs as DecodedLogs; + } + + it('succeeds with no bridge calls and no input balance', async () => { + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls: [], + }); + await callBridgeTransferFromAsync({ bridgeData, sellAmount: ZERO_AMOUNT }); + }); + + it('succeeds with bridge calls and no input balance', async () => { + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls: allBridgeCalls, + }); + await callBridgeTransferFromAsync({ bridgeData, sellAmount: ZERO_AMOUNT }); + }); + + it('fails with no bridge calls and an input balance', async () => { + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls: [], + }); + return expect(callBridgeTransferFromAsync({ bridgeData, sellAmount: new BigNumber(1) })).to.revertWith( + INCOMPLETE_FILL_REVERT, + ); + }); + + it('fails if entire input token balance is not consumed', async () => { + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls: allBridgeCalls, + }); + return expect( + callBridgeTransferFromAsync({ + bridgeData, + sellAmount: totalFillableInputAmount.plus(1), + }), + ).to.revertWith(INCOMPLETE_FILL_REVERT); + }); + + it('succeeds with one bridge call', async () => { + const calls = goodBridgeCalls.slice(0, 1); + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callBridgeTransferFromAsync({ bridgeData, sellAmount: calls[0].inputTokenAmount }); + }); + + it('succeeds with many bridge calls', async () => { + const calls = goodBridgeCalls; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callBridgeTransferFromAsync({ bridgeData }); + }); + + it('swallows a failing bridge call', async () => { + const calls = _.shuffle([...goodBridgeCalls, failingBridgeCall]); + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callBridgeTransferFromAsync({ bridgeData }); + }); + + it('consumes input tokens for output tokens', async () => { + const calls = allBridgeCalls; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callBridgeTransferFromAsync({ bridgeData }); + const currentBridgeInputBalance = await testContract + .balanceOf(inputToken, testContract.address) + .callAsync(); + expect(currentBridgeInputBalance).to.bignumber.eq(0); + const currentRecipientOutputBalance = await testContract + .balanceOf(outputToken, DEFAULTS.toAddress) + .callAsync(); + expect(currentRecipientOutputBalance).to.bignumber.eq(totalFillableOutputAmount); + }); + + it('emits failure events for failing bridge calls', async () => { + const calls = [revertingBridgeCall, failingBridgeCall, ...goodBridgeCalls]; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + verifyEventsFromLogs( + logs, + [revertingBridgeCall, failingBridgeCall].map(c => ({ + inputToken, + outputToken, + target: c.target, + inputTokenAmount: c.inputTokenAmount, + outputTokenAmount: c.outputTokenAmount, + })), + DexForwarderBridgeEvents.DexForwarderBridgeCallFailed, + ); + }); + + it("transfers only up to each call's input amount to each bridge", async () => { + const calls = goodBridgeCalls; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + const btfs = filterLogsToArguments(logs, TestEvents.BridgeTransferFromCalled); + for (const [call, btf] of shortZip(goodBridgeCalls, btfs)) { + expect(btf.inputTokenBalance).to.bignumber.eq(call.inputTokenAmount); + } + }); + + it('transfers only up to outstanding sell amount to each bridge', async () => { + // Prepend an extra bridge call. + const calls = [ + await createBridgeCallAsync({ + callFields: { + inputTokenAmount: new BigNumber(1), + outputTokenAmount: new BigNumber(1), + }, + }), + ...goodBridgeCalls, + ]; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + const btfs = filterLogsToArguments(logs, TestEvents.BridgeTransferFromCalled); + expect(btfs).to.be.length(goodBridgeCalls.length + 1); + // The last call will receive 1 less token. + const lastCall = calls.slice(-1)[0]; + const lastBtf = btfs.slice(-1)[0]; + expect(lastBtf.inputTokenBalance).to.bignumber.eq(lastCall.inputTokenAmount.minus(1)); + }); + + it('recoups funds from a bridge that fails', async () => { + // Prepend a call that will take the whole input amount but will + // fail. + const badCall = await createBridgeCallAsync({ + callFields: { inputTokenAmount: totalFillableInputAmount }, + returnCode: BRIDGE_FAILURE, + }); + const calls = [badCall, ...goodBridgeCalls]; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + verifyEventsFromLogs( + logs, + [ + { + inputToken, + outputToken, + target: badCall.target, + inputTokenAmount: badCall.inputTokenAmount, + outputTokenAmount: badCall.outputTokenAmount, + }, + ], + TestEvents.DexForwarderBridgeCallFailed, + ); + const btfs = filterLogsToArguments(logs, TestEvents.BridgeTransferFromCalled); + expect(btfs).to.be.length(goodBridgeCalls.length); + }); + + it('recoups funds from a bridge that reverts', async () => { + // Prepend a call that will take the whole input amount but will + // revert. + const badCall = await createBridgeCallAsync({ + callFields: { inputTokenAmount: totalFillableInputAmount }, + revertError: BRIDGE_REVERT_ERROR, + }); + const calls = [badCall, ...goodBridgeCalls]; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + verifyEventsFromLogs( + logs, + [ + { + inputToken, + outputToken, + target: badCall.target, + inputTokenAmount: badCall.inputTokenAmount, + outputTokenAmount: badCall.outputTokenAmount, + }, + ], + TestEvents.DexForwarderBridgeCallFailed, + ); + const btfs = filterLogsToArguments(logs, TestEvents.BridgeTransferFromCalled); + expect(btfs).to.be.length(goodBridgeCalls.length); + }); + + it('recoups funds from a bridge that under-pays', async () => { + // Prepend a call that will take the whole input amount but will + // underpay the output amount.. + const badCall = await createBridgeCallAsync({ + callFields: { + inputTokenAmount: totalFillableInputAmount, + outputTokenAmount: new BigNumber(2), + }, + outputFillAmount: new BigNumber(1), + }); + const calls = [badCall, ...goodBridgeCalls]; + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + const logs = await callBridgeTransferFromAsync({ bridgeData }); + verifyEventsFromLogs( + logs, + [ + { + inputToken, + outputToken, + target: badCall.target, + inputTokenAmount: badCall.inputTokenAmount, + outputTokenAmount: badCall.outputTokenAmount, + }, + ], + TestEvents.DexForwarderBridgeCallFailed, + ); + const btfs = filterLogsToArguments(logs, TestEvents.BridgeTransferFromCalled); + expect(btfs).to.be.length(goodBridgeCalls.length); + }); + }); +}); diff --git a/contracts/asset-proxy/test/wrappers.ts b/contracts/asset-proxy/test/wrappers.ts index 679c9c033b..7026456250 100644 --- a/contracts/asset-proxy/test/wrappers.ts +++ b/contracts/asset-proxy/test/wrappers.ts @@ -5,6 +5,7 @@ */ export * from '../test/generated-wrappers/chai_bridge'; export * from '../test/generated-wrappers/curve_bridge'; +export * from '../test/generated-wrappers/dex_forwarder_bridge'; export * from '../test/generated-wrappers/dydx_bridge'; export * from '../test/generated-wrappers/erc1155_proxy'; export * from '../test/generated-wrappers/erc20_bridge_proxy'; @@ -33,6 +34,7 @@ export * from '../test/generated-wrappers/multi_asset_proxy'; export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/static_call_proxy'; export * from '../test/generated-wrappers/test_chai_bridge'; +export * from '../test/generated-wrappers/test_dex_forwarder_bridge'; export * from '../test/generated-wrappers/test_dydx_bridge'; export * from '../test/generated-wrappers/test_erc20_bridge'; export * from '../test/generated-wrappers/test_eth2_dai_bridge'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index f42c66b180..bbdde8b013 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -5,6 +5,7 @@ "files": [ "generated-artifacts/ChaiBridge.json", "generated-artifacts/CurveBridge.json", + "generated-artifacts/DexForwarderBridge.json", "generated-artifacts/DydxBridge.json", "generated-artifacts/ERC1155Proxy.json", "generated-artifacts/ERC20BridgeProxy.json", @@ -33,6 +34,7 @@ "generated-artifacts/Ownable.json", "generated-artifacts/StaticCallProxy.json", "generated-artifacts/TestChaiBridge.json", + "generated-artifacts/TestDexForwarderBridge.json", "generated-artifacts/TestDydxBridge.json", "generated-artifacts/TestERC20Bridge.json", "generated-artifacts/TestEth2DaiBridge.json", @@ -42,6 +44,7 @@ "generated-artifacts/UniswapBridge.json", "test/generated-artifacts/ChaiBridge.json", "test/generated-artifacts/CurveBridge.json", + "test/generated-artifacts/DexForwarderBridge.json", "test/generated-artifacts/DydxBridge.json", "test/generated-artifacts/ERC1155Proxy.json", "test/generated-artifacts/ERC20BridgeProxy.json", @@ -70,6 +73,7 @@ "test/generated-artifacts/Ownable.json", "test/generated-artifacts/StaticCallProxy.json", "test/generated-artifacts/TestChaiBridge.json", + "test/generated-artifacts/TestDexForwarderBridge.json", "test/generated-artifacts/TestDydxBridge.json", "test/generated-artifacts/TestERC20Bridge.json", "test/generated-artifacts/TestEth2DaiBridge.json",