@0x/contracts-asset-proxy: Create DexForwarderBridge bridge contract.
				
					
				
			This commit is contained in:
		| @@ -17,6 +17,10 @@ | |||||||
|             { |             { | ||||||
|                 "note": "Added `MixinGasToken` allowing Gas Tokens to be freed", |                 "note": "Added `MixinGasToken` allowing Gas Tokens to be freed", | ||||||
|                 "pr": 2523 |                 "pr": 2523 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "note": "Add `DexForwaderBridge` bridge contract.", | ||||||
|  |                 "pr": 2525 | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										220
									
								
								contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -38,7 +38,7 @@ | |||||||
|         "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" |         "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" | ||||||
|     }, |     }, | ||||||
|     "config": { |     "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." |         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." | ||||||
|     }, |     }, | ||||||
|     "repository": { |     "repository": { | ||||||
| @@ -52,6 +52,7 @@ | |||||||
|     "homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md", |     "homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md", | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@0x/abi-gen": "^5.2.2", |         "@0x/abi-gen": "^5.2.2", | ||||||
|  |         "@0x/contract-wrappers": "^13.6.3", | ||||||
|         "@0x/contracts-gen": "^2.0.8", |         "@0x/contracts-gen": "^2.0.8", | ||||||
|         "@0x/contracts-test-utils": "^5.3.2", |         "@0x/contracts-test-utils": "^5.3.2", | ||||||
|         "@0x/contracts-utils": "^4.4.3", |         "@0x/contracts-utils": "^4.4.3", | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types'; | |||||||
|  |  | ||||||
| import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json'; | import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json'; | ||||||
| import * as CurveBridge from '../generated-artifacts/CurveBridge.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 DydxBridge from '../generated-artifacts/DydxBridge.json'; | ||||||
| import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json'; | import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json'; | ||||||
| import * as ERC20BridgeProxy from '../generated-artifacts/ERC20BridgeProxy.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 Ownable from '../generated-artifacts/Ownable.json'; | ||||||
| import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; | import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; | ||||||
| import * as TestChaiBridge from '../generated-artifacts/TestChaiBridge.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 TestDydxBridge from '../generated-artifacts/TestDydxBridge.json'; | ||||||
| import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json'; | import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json'; | ||||||
| import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json'; | import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json'; | ||||||
| @@ -54,6 +56,7 @@ export const artifacts = { | |||||||
|     StaticCallProxy: StaticCallProxy as ContractArtifact, |     StaticCallProxy: StaticCallProxy as ContractArtifact, | ||||||
|     ChaiBridge: ChaiBridge as ContractArtifact, |     ChaiBridge: ChaiBridge as ContractArtifact, | ||||||
|     CurveBridge: CurveBridge as ContractArtifact, |     CurveBridge: CurveBridge as ContractArtifact, | ||||||
|  |     DexForwarderBridge: DexForwarderBridge as ContractArtifact, | ||||||
|     DydxBridge: DydxBridge as ContractArtifact, |     DydxBridge: DydxBridge as ContractArtifact, | ||||||
|     Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, |     Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, | ||||||
|     KyberBridge: KyberBridge as ContractArtifact, |     KyberBridge: KyberBridge as ContractArtifact, | ||||||
| @@ -74,6 +77,7 @@ export const artifacts = { | |||||||
|     IUniswapExchange: IUniswapExchange as ContractArtifact, |     IUniswapExchange: IUniswapExchange as ContractArtifact, | ||||||
|     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, |     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, | ||||||
|     TestChaiBridge: TestChaiBridge as ContractArtifact, |     TestChaiBridge: TestChaiBridge as ContractArtifact, | ||||||
|  |     TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, | ||||||
|     TestDydxBridge: TestDydxBridge as ContractArtifact, |     TestDydxBridge: TestDydxBridge as ContractArtifact, | ||||||
|     TestERC20Bridge: TestERC20Bridge as ContractArtifact, |     TestERC20Bridge: TestERC20Bridge as ContractArtifact, | ||||||
|     TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, |     TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								contracts/asset-proxy/src/dex_forwarder_bridge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								contracts/asset-proxy/src/dex_forwarder_bridge.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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' }, | ||||||
|  |         ], | ||||||
|  |     }, | ||||||
|  | ]); | ||||||
| @@ -88,3 +88,4 @@ export { | |||||||
| } from './asset_data'; | } from './asset_data'; | ||||||
|  |  | ||||||
| export * from './dydx_bridge_encoder'; | export * from './dydx_bridge_encoder'; | ||||||
|  | export * from './dex_forwarder_bridge'; | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|  */ |  */ | ||||||
| export * from '../generated-wrappers/chai_bridge'; | export * from '../generated-wrappers/chai_bridge'; | ||||||
| export * from '../generated-wrappers/curve_bridge'; | export * from '../generated-wrappers/curve_bridge'; | ||||||
|  | export * from '../generated-wrappers/dex_forwarder_bridge'; | ||||||
| export * from '../generated-wrappers/dydx_bridge'; | export * from '../generated-wrappers/dydx_bridge'; | ||||||
| export * from '../generated-wrappers/erc1155_proxy'; | export * from '../generated-wrappers/erc1155_proxy'; | ||||||
| export * from '../generated-wrappers/erc20_bridge_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/ownable'; | ||||||
| export * from '../generated-wrappers/static_call_proxy'; | export * from '../generated-wrappers/static_call_proxy'; | ||||||
| export * from '../generated-wrappers/test_chai_bridge'; | 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_dydx_bridge'; | ||||||
| export * from '../generated-wrappers/test_erc20_bridge'; | export * from '../generated-wrappers/test_erc20_bridge'; | ||||||
| export * from '../generated-wrappers/test_eth2_dai_bridge'; | export * from '../generated-wrappers/test_eth2_dai_bridge'; | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types'; | |||||||
|  |  | ||||||
| import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json'; | import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json'; | ||||||
| import * as CurveBridge from '../test/generated-artifacts/CurveBridge.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 DydxBridge from '../test/generated-artifacts/DydxBridge.json'; | ||||||
| import * as ERC1155Proxy from '../test/generated-artifacts/ERC1155Proxy.json'; | import * as ERC1155Proxy from '../test/generated-artifacts/ERC1155Proxy.json'; | ||||||
| import * as ERC20BridgeProxy from '../test/generated-artifacts/ERC20BridgeProxy.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 Ownable from '../test/generated-artifacts/Ownable.json'; | ||||||
| import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; | import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; | ||||||
| import * as TestChaiBridge from '../test/generated-artifacts/TestChaiBridge.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 TestDydxBridge from '../test/generated-artifacts/TestDydxBridge.json'; | ||||||
| import * as TestERC20Bridge from '../test/generated-artifacts/TestERC20Bridge.json'; | import * as TestERC20Bridge from '../test/generated-artifacts/TestERC20Bridge.json'; | ||||||
| import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; | import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; | ||||||
| @@ -54,6 +56,7 @@ export const artifacts = { | |||||||
|     StaticCallProxy: StaticCallProxy as ContractArtifact, |     StaticCallProxy: StaticCallProxy as ContractArtifact, | ||||||
|     ChaiBridge: ChaiBridge as ContractArtifact, |     ChaiBridge: ChaiBridge as ContractArtifact, | ||||||
|     CurveBridge: CurveBridge as ContractArtifact, |     CurveBridge: CurveBridge as ContractArtifact, | ||||||
|  |     DexForwarderBridge: DexForwarderBridge as ContractArtifact, | ||||||
|     DydxBridge: DydxBridge as ContractArtifact, |     DydxBridge: DydxBridge as ContractArtifact, | ||||||
|     Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, |     Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, | ||||||
|     KyberBridge: KyberBridge as ContractArtifact, |     KyberBridge: KyberBridge as ContractArtifact, | ||||||
| @@ -74,6 +77,7 @@ export const artifacts = { | |||||||
|     IUniswapExchange: IUniswapExchange as ContractArtifact, |     IUniswapExchange: IUniswapExchange as ContractArtifact, | ||||||
|     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, |     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, | ||||||
|     TestChaiBridge: TestChaiBridge as ContractArtifact, |     TestChaiBridge: TestChaiBridge as ContractArtifact, | ||||||
|  |     TestDexForwarderBridge: TestDexForwarderBridge as ContractArtifact, | ||||||
|     TestDydxBridge: TestDydxBridge as ContractArtifact, |     TestDydxBridge: TestDydxBridge as ContractArtifact, | ||||||
|     TestERC20Bridge: TestERC20Bridge as ContractArtifact, |     TestERC20Bridge: TestERC20Bridge as ContractArtifact, | ||||||
|     TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, |     TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, | ||||||
|   | |||||||
							
								
								
									
										385
									
								
								contracts/asset-proxy/test/dex_forwarder_bridge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										385
									
								
								contracts/asset-proxy/test/dex_forwarder_bridge.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<TResult>(fnCall: ContractTxFunctionObj<TResult>): Promise<TResult> { | ||||||
|  |         const result = await fnCall.callAsync(); | ||||||
|  |         await fnCall.awaitTransactionSuccessAsync({}, { shouldValidate: false }); | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getRandomBridgeCall( | ||||||
|  |         bridgeAddress: string, | ||||||
|  |         fields: Partial<DexForwaderBridgeCall> = {}, | ||||||
|  |     ): 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<void> { | ||||||
|  |             await testContract | ||||||
|  |                 .setTokenBalance(inputToken, testContract.address, amount) | ||||||
|  |                 .awaitTransactionSuccessAsync({}, { shouldValidate: false }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         async function createBridgeCallAsync( | ||||||
|  |             opts: Partial<{ | ||||||
|  |                 returnCode: string; | ||||||
|  |                 revertError: string; | ||||||
|  |                 callFields: Partial<DexForwaderBridgeCall>; | ||||||
|  |                 outputFillAmount: BigNumber; | ||||||
|  |             }>, | ||||||
|  |         ): Promise<DexForwaderBridgeCall> { | ||||||
|  |             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<DecodedLogs> { | ||||||
|  |             // 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<BtfCalledEventArgs>(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<BtfCalledEventArgs>(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<BtfCalledEventArgs>(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<BtfCalledEventArgs>(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<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled); | ||||||
|  |             expect(btfs).to.be.length(goodBridgeCalls.length); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @@ -5,6 +5,7 @@ | |||||||
|  */ |  */ | ||||||
| export * from '../test/generated-wrappers/chai_bridge'; | export * from '../test/generated-wrappers/chai_bridge'; | ||||||
| export * from '../test/generated-wrappers/curve_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/dydx_bridge'; | ||||||
| export * from '../test/generated-wrappers/erc1155_proxy'; | export * from '../test/generated-wrappers/erc1155_proxy'; | ||||||
| export * from '../test/generated-wrappers/erc20_bridge_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/ownable'; | ||||||
| export * from '../test/generated-wrappers/static_call_proxy'; | export * from '../test/generated-wrappers/static_call_proxy'; | ||||||
| export * from '../test/generated-wrappers/test_chai_bridge'; | 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_dydx_bridge'; | ||||||
| export * from '../test/generated-wrappers/test_erc20_bridge'; | export * from '../test/generated-wrappers/test_erc20_bridge'; | ||||||
| export * from '../test/generated-wrappers/test_eth2_dai_bridge'; | export * from '../test/generated-wrappers/test_eth2_dai_bridge'; | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|     "files": [ |     "files": [ | ||||||
|         "generated-artifacts/ChaiBridge.json", |         "generated-artifacts/ChaiBridge.json", | ||||||
|         "generated-artifacts/CurveBridge.json", |         "generated-artifacts/CurveBridge.json", | ||||||
|  |         "generated-artifacts/DexForwarderBridge.json", | ||||||
|         "generated-artifacts/DydxBridge.json", |         "generated-artifacts/DydxBridge.json", | ||||||
|         "generated-artifacts/ERC1155Proxy.json", |         "generated-artifacts/ERC1155Proxy.json", | ||||||
|         "generated-artifacts/ERC20BridgeProxy.json", |         "generated-artifacts/ERC20BridgeProxy.json", | ||||||
| @@ -33,6 +34,7 @@ | |||||||
|         "generated-artifacts/Ownable.json", |         "generated-artifacts/Ownable.json", | ||||||
|         "generated-artifacts/StaticCallProxy.json", |         "generated-artifacts/StaticCallProxy.json", | ||||||
|         "generated-artifacts/TestChaiBridge.json", |         "generated-artifacts/TestChaiBridge.json", | ||||||
|  |         "generated-artifacts/TestDexForwarderBridge.json", | ||||||
|         "generated-artifacts/TestDydxBridge.json", |         "generated-artifacts/TestDydxBridge.json", | ||||||
|         "generated-artifacts/TestERC20Bridge.json", |         "generated-artifacts/TestERC20Bridge.json", | ||||||
|         "generated-artifacts/TestEth2DaiBridge.json", |         "generated-artifacts/TestEth2DaiBridge.json", | ||||||
| @@ -42,6 +44,7 @@ | |||||||
|         "generated-artifacts/UniswapBridge.json", |         "generated-artifacts/UniswapBridge.json", | ||||||
|         "test/generated-artifacts/ChaiBridge.json", |         "test/generated-artifacts/ChaiBridge.json", | ||||||
|         "test/generated-artifacts/CurveBridge.json", |         "test/generated-artifacts/CurveBridge.json", | ||||||
|  |         "test/generated-artifacts/DexForwarderBridge.json", | ||||||
|         "test/generated-artifacts/DydxBridge.json", |         "test/generated-artifacts/DydxBridge.json", | ||||||
|         "test/generated-artifacts/ERC1155Proxy.json", |         "test/generated-artifacts/ERC1155Proxy.json", | ||||||
|         "test/generated-artifacts/ERC20BridgeProxy.json", |         "test/generated-artifacts/ERC20BridgeProxy.json", | ||||||
| @@ -70,6 +73,7 @@ | |||||||
|         "test/generated-artifacts/Ownable.json", |         "test/generated-artifacts/Ownable.json", | ||||||
|         "test/generated-artifacts/StaticCallProxy.json", |         "test/generated-artifacts/StaticCallProxy.json", | ||||||
|         "test/generated-artifacts/TestChaiBridge.json", |         "test/generated-artifacts/TestChaiBridge.json", | ||||||
|  |         "test/generated-artifacts/TestDexForwarderBridge.json", | ||||||
|         "test/generated-artifacts/TestDydxBridge.json", |         "test/generated-artifacts/TestDydxBridge.json", | ||||||
|         "test/generated-artifacts/TestERC20Bridge.json", |         "test/generated-artifacts/TestERC20Bridge.json", | ||||||
|         "test/generated-artifacts/TestEth2DaiBridge.json", |         "test/generated-artifacts/TestEth2DaiBridge.json", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user