Merge branch 'development' into feat/add-cream
This commit is contained in:
		| @@ -29,6 +29,10 @@ | ||||
|             { | ||||
|                 "note": "Added `CreamBridge`", | ||||
|                 "pr": 2715 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added `ShellBridge`", | ||||
|                 "pr": 2722 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										96
									
								
								contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| /* | ||||
|  | ||||
|   Copyright 2019 ZeroEx Intl. | ||||
|  | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
|  | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.5.9; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; | ||||
| import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; | ||||
| import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; | ||||
| import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; | ||||
| import "../interfaces/IERC20Bridge.sol"; | ||||
| import "../interfaces/IShell.sol"; | ||||
|  | ||||
|  | ||||
| contract ShellBridge is | ||||
|     IERC20Bridge, | ||||
|     IWallet, | ||||
|     DeploymentConstants | ||||
| { | ||||
|  | ||||
|     /// @dev Swaps specified tokens against the Shell contract | ||||
|     /// @param toTokenAddress The token to give to `to`. | ||||
|     /// @param from The maker (this contract). | ||||
|     /// @param to The recipient of the bought tokens. | ||||
|     /// @param amount Minimum amount of `toTokenAddress` tokens to buy. | ||||
|     /// @param bridgeData The abi-encoded "from" token address. | ||||
|     /// @return success The magic bytes if successful. | ||||
|     // solhint-disable no-unused-vars | ||||
|     function bridgeTransferFrom( | ||||
|         address toTokenAddress, | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 amount, | ||||
|         bytes calldata bridgeData | ||||
|     ) | ||||
|         external | ||||
|         returns (bytes4 success) | ||||
|     { | ||||
|         // Decode the bridge data to get the `fromTokenAddress`. | ||||
|         (address fromTokenAddress) = abi.decode(bridgeData, (address)); | ||||
|  | ||||
|         uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); | ||||
|         IShell exchange = IShell(_getShellAddress()); | ||||
|         // Grant an allowance to the exchange to spend `fromTokenAddress` token. | ||||
|         LibERC20Token.approveIfBelow(fromTokenAddress, address(exchange), fromTokenBalance); | ||||
|  | ||||
|         // Try to sell all of this contract's `fromTokenAddress` token balance. | ||||
|         uint256 boughtAmount = exchange.originSwap( | ||||
|             fromTokenAddress, | ||||
|             toTokenAddress, | ||||
|             fromTokenBalance, | ||||
|             amount, // min amount | ||||
|             block.timestamp + 1 | ||||
|         ); | ||||
|         LibERC20Token.transfer(toTokenAddress, to, boughtAmount); | ||||
|  | ||||
|         emit ERC20BridgeTransfer( | ||||
|             fromTokenAddress, | ||||
|             toTokenAddress, | ||||
|             fromTokenBalance, | ||||
|             boughtAmount, | ||||
|             from, | ||||
|             to | ||||
|         ); | ||||
|         return BRIDGE_SUCCESS; | ||||
|     } | ||||
|  | ||||
|     /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker | ||||
|     ///      and sign for itself in orders. Always succeeds. | ||||
|     /// @return magicValue Magic success bytes, always. | ||||
|     function isValidSignature( | ||||
|         bytes32, | ||||
|         bytes calldata | ||||
|     ) | ||||
|         external | ||||
|         view | ||||
|         returns (bytes4 magicValue) | ||||
|     { | ||||
|         return LEGACY_WALLET_MAGIC_VALUE; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								contracts/asset-proxy/contracts/src/interfaces/IShell.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								contracts/asset-proxy/contracts/src/interfaces/IShell.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| /* | ||||
|  | ||||
|   Copyright 2020 ZeroEx Intl. | ||||
|  | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
|  | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.5.9; | ||||
|  | ||||
|  | ||||
| interface IShell { | ||||
|  | ||||
|     function originSwap( | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 fromAmount, | ||||
|         uint256 minTargetAmount, | ||||
|         uint256 deadline | ||||
|     ) | ||||
|         external | ||||
|         returns (uint256 toAmount); | ||||
| } | ||||
|  | ||||
| @@ -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/@(BalancerBridge|BancorBridge|ChaiBridge|CreamBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IBancorNetwork|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IMStable|IMooniswap|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MStableBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MooniswapBridge|MultiAssetProxy|Ownable|StaticCallProxy|SushiSwapBridge|TestBancorBridge|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", | ||||
|         "abis": "./test/generated-artifacts/@(BalancerBridge|BancorBridge|ChaiBridge|CreamBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IBancorNetwork|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IMStable|IMooniswap|IShell|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MStableBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MooniswapBridge|MultiAssetProxy|Ownable|ShellBridge|StaticCallProxy|SushiSwapBridge|TestBancorBridge|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", | ||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." | ||||
|     }, | ||||
|     "repository": { | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import * as IGasToken from '../generated-artifacts/IGasToken.json'; | ||||
| import * as IKyberNetworkProxy from '../generated-artifacts/IKyberNetworkProxy.json'; | ||||
| import * as IMooniswap from '../generated-artifacts/IMooniswap.json'; | ||||
| import * as IMStable from '../generated-artifacts/IMStable.json'; | ||||
| import * as IShell from '../generated-artifacts/IShell.json'; | ||||
| import * as IUniswapExchange from '../generated-artifacts/IUniswapExchange.json'; | ||||
| import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json'; | ||||
| import * as IUniswapV2Router01 from '../generated-artifacts/IUniswapV2Router01.json'; | ||||
| @@ -44,6 +45,7 @@ import * as MooniswapBridge from '../generated-artifacts/MooniswapBridge.json'; | ||||
| import * as MStableBridge from '../generated-artifacts/MStableBridge.json'; | ||||
| import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; | ||||
| import * as Ownable from '../generated-artifacts/Ownable.json'; | ||||
| import * as ShellBridge from '../generated-artifacts/ShellBridge.json'; | ||||
| import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; | ||||
| import * as SushiSwapBridge from '../generated-artifacts/SushiSwapBridge.json'; | ||||
| import * as TestBancorBridge from '../generated-artifacts/TestBancorBridge.json'; | ||||
| @@ -80,6 +82,7 @@ export const artifacts = { | ||||
|     MStableBridge: MStableBridge as ContractArtifact, | ||||
|     MixinGasToken: MixinGasToken as ContractArtifact, | ||||
|     MooniswapBridge: MooniswapBridge as ContractArtifact, | ||||
|     ShellBridge: ShellBridge as ContractArtifact, | ||||
|     SushiSwapBridge: SushiSwapBridge as ContractArtifact, | ||||
|     UniswapBridge: UniswapBridge as ContractArtifact, | ||||
|     UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, | ||||
| @@ -99,6 +102,7 @@ export const artifacts = { | ||||
|     IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, | ||||
|     IMStable: IMStable as ContractArtifact, | ||||
|     IMooniswap: IMooniswap as ContractArtifact, | ||||
|     IShell: IShell as ContractArtifact, | ||||
|     IUniswapExchange: IUniswapExchange as ContractArtifact, | ||||
|     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, | ||||
|     IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, | ||||
|   | ||||
| @@ -31,6 +31,7 @@ export * from '../generated-wrappers/i_gas_token'; | ||||
| export * from '../generated-wrappers/i_kyber_network_proxy'; | ||||
| export * from '../generated-wrappers/i_m_stable'; | ||||
| export * from '../generated-wrappers/i_mooniswap'; | ||||
| export * from '../generated-wrappers/i_shell'; | ||||
| export * from '../generated-wrappers/i_uniswap_exchange'; | ||||
| export * from '../generated-wrappers/i_uniswap_exchange_factory'; | ||||
| export * from '../generated-wrappers/i_uniswap_v2_router01'; | ||||
| @@ -42,6 +43,7 @@ export * from '../generated-wrappers/mixin_gas_token'; | ||||
| export * from '../generated-wrappers/mooniswap_bridge'; | ||||
| export * from '../generated-wrappers/multi_asset_proxy'; | ||||
| export * from '../generated-wrappers/ownable'; | ||||
| export * from '../generated-wrappers/shell_bridge'; | ||||
| export * from '../generated-wrappers/static_call_proxy'; | ||||
| export * from '../generated-wrappers/sushi_swap_bridge'; | ||||
| export * from '../generated-wrappers/test_bancor_bridge'; | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; | ||||
| import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; | ||||
| import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; | ||||
| import * as IMStable from '../test/generated-artifacts/IMStable.json'; | ||||
| import * as IShell from '../test/generated-artifacts/IShell.json'; | ||||
| import * as IUniswapExchange from '../test/generated-artifacts/IUniswapExchange.json'; | ||||
| import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json'; | ||||
| import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; | ||||
| @@ -44,6 +45,7 @@ import * as MooniswapBridge from '../test/generated-artifacts/MooniswapBridge.js | ||||
| import * as MStableBridge from '../test/generated-artifacts/MStableBridge.json'; | ||||
| import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.json'; | ||||
| import * as Ownable from '../test/generated-artifacts/Ownable.json'; | ||||
| import * as ShellBridge from '../test/generated-artifacts/ShellBridge.json'; | ||||
| import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; | ||||
| import * as SushiSwapBridge from '../test/generated-artifacts/SushiSwapBridge.json'; | ||||
| import * as TestBancorBridge from '../test/generated-artifacts/TestBancorBridge.json'; | ||||
| @@ -80,6 +82,7 @@ export const artifacts = { | ||||
|     MStableBridge: MStableBridge as ContractArtifact, | ||||
|     MixinGasToken: MixinGasToken as ContractArtifact, | ||||
|     MooniswapBridge: MooniswapBridge as ContractArtifact, | ||||
|     ShellBridge: ShellBridge as ContractArtifact, | ||||
|     SushiSwapBridge: SushiSwapBridge as ContractArtifact, | ||||
|     UniswapBridge: UniswapBridge as ContractArtifact, | ||||
|     UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, | ||||
| @@ -99,6 +102,7 @@ export const artifacts = { | ||||
|     IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, | ||||
|     IMStable: IMStable as ContractArtifact, | ||||
|     IMooniswap: IMooniswap as ContractArtifact, | ||||
|     IShell: IShell as ContractArtifact, | ||||
|     IUniswapExchange: IUniswapExchange as ContractArtifact, | ||||
|     IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, | ||||
|     IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, | ||||
|   | ||||
| @@ -31,6 +31,7 @@ export * from '../test/generated-wrappers/i_gas_token'; | ||||
| export * from '../test/generated-wrappers/i_kyber_network_proxy'; | ||||
| export * from '../test/generated-wrappers/i_m_stable'; | ||||
| export * from '../test/generated-wrappers/i_mooniswap'; | ||||
| export * from '../test/generated-wrappers/i_shell'; | ||||
| export * from '../test/generated-wrappers/i_uniswap_exchange'; | ||||
| export * from '../test/generated-wrappers/i_uniswap_exchange_factory'; | ||||
| export * from '../test/generated-wrappers/i_uniswap_v2_router01'; | ||||
| @@ -42,6 +43,7 @@ export * from '../test/generated-wrappers/mixin_gas_token'; | ||||
| export * from '../test/generated-wrappers/mooniswap_bridge'; | ||||
| export * from '../test/generated-wrappers/multi_asset_proxy'; | ||||
| export * from '../test/generated-wrappers/ownable'; | ||||
| export * from '../test/generated-wrappers/shell_bridge'; | ||||
| export * from '../test/generated-wrappers/static_call_proxy'; | ||||
| export * from '../test/generated-wrappers/sushi_swap_bridge'; | ||||
| export * from '../test/generated-wrappers/test_bancor_bridge'; | ||||
|   | ||||
| @@ -31,6 +31,7 @@ | ||||
|         "generated-artifacts/IKyberNetworkProxy.json", | ||||
|         "generated-artifacts/IMStable.json", | ||||
|         "generated-artifacts/IMooniswap.json", | ||||
|         "generated-artifacts/IShell.json", | ||||
|         "generated-artifacts/IUniswapExchange.json", | ||||
|         "generated-artifacts/IUniswapExchangeFactory.json", | ||||
|         "generated-artifacts/IUniswapV2Router01.json", | ||||
| @@ -42,6 +43,7 @@ | ||||
|         "generated-artifacts/MooniswapBridge.json", | ||||
|         "generated-artifacts/MultiAssetProxy.json", | ||||
|         "generated-artifacts/Ownable.json", | ||||
|         "generated-artifacts/ShellBridge.json", | ||||
|         "generated-artifacts/StaticCallProxy.json", | ||||
|         "generated-artifacts/SushiSwapBridge.json", | ||||
|         "generated-artifacts/TestBancorBridge.json", | ||||
| @@ -84,6 +86,7 @@ | ||||
|         "test/generated-artifacts/IKyberNetworkProxy.json", | ||||
|         "test/generated-artifacts/IMStable.json", | ||||
|         "test/generated-artifacts/IMooniswap.json", | ||||
|         "test/generated-artifacts/IShell.json", | ||||
|         "test/generated-artifacts/IUniswapExchange.json", | ||||
|         "test/generated-artifacts/IUniswapExchangeFactory.json", | ||||
|         "test/generated-artifacts/IUniswapV2Router01.json", | ||||
| @@ -95,6 +98,7 @@ | ||||
|         "test/generated-artifacts/MooniswapBridge.json", | ||||
|         "test/generated-artifacts/MultiAssetProxy.json", | ||||
|         "test/generated-artifacts/Ownable.json", | ||||
|         "test/generated-artifacts/ShellBridge.json", | ||||
|         "test/generated-artifacts/StaticCallProxy.json", | ||||
|         "test/generated-artifacts/SushiSwapBridge.json", | ||||
|         "test/generated-artifacts/TestBancorBridge.json", | ||||
|   | ||||
| @@ -56,6 +56,8 @@ contract DeploymentConstants { | ||||
|     address constant private MUSD_ADDRESS = 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5; | ||||
|     /// @dev Mainnet address of the Mooniswap Registry contract | ||||
|     address constant private MOONISWAP_REGISTRY = 0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303; | ||||
|     /// @dev Mainnet address of the Shell contract | ||||
|     address constant private SHELL_CONTRACT = 0x2E703D658f8dd21709a7B458967aB4081F8D3d05; | ||||
|  | ||||
|     // // Ropsten addresses /////////////////////////////////////////////////////// | ||||
|     // /// @dev Mainnet address of the WETH contract. | ||||
| @@ -296,4 +298,14 @@ contract DeploymentConstants { | ||||
|     { | ||||
|         return MOONISWAP_REGISTRY; | ||||
|     } | ||||
|  | ||||
|     /// @dev An overridable way to retrieve the Shell contract address. | ||||
|     /// @return registry The Shell contract address. | ||||
|     function _getShellAddress() | ||||
|         internal | ||||
|         view | ||||
|         returns (address) | ||||
|     { | ||||
|         return SHELL_CONTRACT; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -57,6 +57,14 @@ | ||||
|             { | ||||
|                 "note": "Fix versioning (`_encodeVersion()`) bug", | ||||
|                 "pr": 2703 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added LiquidityProviderFeature", | ||||
|                 "pr": 2691 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added `Shell` into FQT", | ||||
|                 "pr": 2722 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import "./features/ISignatureValidatorFeature.sol"; | ||||
| import "./features/ITransformERC20Feature.sol"; | ||||
| import "./features/IMetaTransactionsFeature.sol"; | ||||
| import "./features/IUniswapFeature.sol"; | ||||
| import "./features/ILiquidityProviderFeature.sol"; | ||||
|  | ||||
|  | ||||
| /// @dev Interface for a fully featured Exchange Proxy. | ||||
| @@ -36,7 +37,8 @@ interface IZeroEx is | ||||
|     ISignatureValidatorFeature, | ||||
|     ITransformERC20Feature, | ||||
|     IMetaTransactionsFeature, | ||||
|     IUniswapFeature | ||||
|     IUniswapFeature, | ||||
|     ILiquidityProviderFeature | ||||
| { | ||||
|     // solhint-disable state-visibility | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,63 @@ | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
|  | ||||
|  | ||||
| library LibLiquidityProviderRichErrors { | ||||
|  | ||||
|     // solhint-disable func-name-mixedcase | ||||
|  | ||||
|     function LiquidityProviderIncompleteSellError( | ||||
|         address providerAddress, | ||||
|         address makerToken, | ||||
|         address takerToken, | ||||
|         uint256 sellAmount, | ||||
|         uint256 boughtAmount, | ||||
|         uint256 minBuyAmount | ||||
|     ) | ||||
|         internal | ||||
|         pure | ||||
|         returns (bytes memory) | ||||
|     { | ||||
|         return abi.encodeWithSelector( | ||||
|             bytes4(keccak256("LiquidityProviderIncompleteSellError(address,address,address,uint256,uint256,uint256)")), | ||||
|             providerAddress, | ||||
|             makerToken, | ||||
|             takerToken, | ||||
|             sellAmount, | ||||
|             boughtAmount, | ||||
|             minBuyAmount | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function NoLiquidityProviderForMarketError( | ||||
|         address xAsset, | ||||
|         address yAsset | ||||
|     ) | ||||
|         internal | ||||
|         pure | ||||
|         returns (bytes memory) | ||||
|     { | ||||
|         return abi.encodeWithSelector( | ||||
|             bytes4(keccak256("NoLiquidityProviderForMarketError(address,address)")), | ||||
|             xAsset, | ||||
|             yAsset | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
|  | ||||
| /// @dev Feature to swap directly with an on-chain liquidity provider. | ||||
| interface ILiquidityProviderFeature { | ||||
|     event LiquidityProviderForMarketUpdated( | ||||
|         address indexed xAsset, | ||||
|         address indexed yAsset, | ||||
|         address providerAddress | ||||
|     ); | ||||
|  | ||||
|     function sellToLiquidityProvider( | ||||
|         address makerToken, | ||||
|         address takerToken, | ||||
|         address payable recipient, | ||||
|         uint256 sellAmount, | ||||
|         uint256 minBuyAmount | ||||
|     ) | ||||
|         external | ||||
|         payable | ||||
|         returns (uint256 boughtAmount); | ||||
|  | ||||
|     /// @dev Sets address of the liquidity provider for a market given | ||||
|     ///      (xAsset, yAsset). | ||||
|     /// @param xAsset First asset managed by the liquidity provider. | ||||
|     /// @param yAsset Second asset managed by the liquidity provider. | ||||
|     /// @param providerAddress Address of the liquidity provider. | ||||
|     function setLiquidityProviderForMarket( | ||||
|         address xAsset, | ||||
|         address yAsset, | ||||
|         address providerAddress | ||||
|     ) | ||||
|         external; | ||||
|  | ||||
|     /// @dev Returns the address of the liquidity provider for a market given | ||||
|     ///     (xAsset, yAsset), or reverts if pool does not exist. | ||||
|     /// @param xAsset First asset managed by the liquidity provider. | ||||
|     /// @param yAsset Second asset managed by the liquidity provider. | ||||
|     /// @return providerAddress Address of the liquidity provider. | ||||
|     function getLiquidityProviderForMarket( | ||||
|         address xAsset, | ||||
|         address yAsset | ||||
|     ) | ||||
|         external | ||||
|         view | ||||
|         returns (address providerAddress); | ||||
| } | ||||
| @@ -0,0 +1,200 @@ | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; | ||||
| import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; | ||||
| import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; | ||||
| import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; | ||||
| import "../errors/LibLiquidityProviderRichErrors.sol"; | ||||
| import "../fixins/FixinCommon.sol"; | ||||
| import "../migrations/LibMigrate.sol"; | ||||
| import "../storage/LibLiquidityProviderStorage.sol"; | ||||
| import "../vendor/v3/IERC20Bridge.sol"; | ||||
| import "./IFeature.sol"; | ||||
| import "./ILiquidityProviderFeature.sol"; | ||||
| import "./ITokenSpenderFeature.sol"; | ||||
|  | ||||
|  | ||||
| contract LiquidityProviderFeature is | ||||
|     IFeature, | ||||
|     ILiquidityProviderFeature, | ||||
|     FixinCommon | ||||
| { | ||||
|     using LibERC20TokenV06 for IERC20TokenV06; | ||||
|     using LibSafeMathV06 for uint256; | ||||
|     using LibRichErrorsV06 for bytes; | ||||
|  | ||||
|     /// @dev Name of this feature. | ||||
|     string public constant override FEATURE_NAME = "LiquidityProviderFeature"; | ||||
|     /// @dev Version of this feature. | ||||
|     uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); | ||||
|  | ||||
|     /// @dev ETH pseudo-token address. | ||||
|     address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; | ||||
|     /// @dev The WETH contract address. | ||||
|     IEtherTokenV06 public immutable weth; | ||||
|  | ||||
|     /// @dev Store the WETH address in an immutable. | ||||
|     /// @param weth_ The weth token. | ||||
|     constructor(IEtherTokenV06 weth_) | ||||
|         public | ||||
|         FixinCommon() | ||||
|     { | ||||
|         weth = weth_; | ||||
|     } | ||||
|  | ||||
|     /// @dev Initialize and register this feature. | ||||
|     ///      Should be delegatecalled by `Migrate.migrate()`. | ||||
|     /// @return success `LibMigrate.SUCCESS` on success. | ||||
|     function migrate() | ||||
|         external | ||||
|         returns (bytes4 success) | ||||
|     { | ||||
|         _registerFeatureFunction(this.sellToLiquidityProvider.selector); | ||||
|         _registerFeatureFunction(this.setLiquidityProviderForMarket.selector); | ||||
|         _registerFeatureFunction(this.getLiquidityProviderForMarket.selector); | ||||
|         return LibMigrate.MIGRATE_SUCCESS; | ||||
|     } | ||||
|  | ||||
|     function sellToLiquidityProvider( | ||||
|         address makerToken, | ||||
|         address takerToken, | ||||
|         address payable recipient, | ||||
|         uint256 sellAmount, | ||||
|         uint256 minBuyAmount | ||||
|     ) | ||||
|         external | ||||
|         override | ||||
|         payable | ||||
|         returns (uint256 boughtAmount) | ||||
|     { | ||||
|         address providerAddress = getLiquidityProviderForMarket(makerToken, takerToken); | ||||
|         if (recipient == address(0)) { | ||||
|             recipient = msg.sender; | ||||
|         } | ||||
|  | ||||
|         if (takerToken == ETH_TOKEN_ADDRESS) { | ||||
|             // Wrap ETH. | ||||
|             weth.deposit{value: sellAmount}(); | ||||
|             weth.transfer(providerAddress, sellAmount); | ||||
|         } else { | ||||
|             ITokenSpenderFeature(address(this))._spendERC20Tokens( | ||||
|                 IERC20TokenV06(takerToken), | ||||
|                 msg.sender, | ||||
|                 providerAddress, | ||||
|                 sellAmount | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if (makerToken == ETH_TOKEN_ADDRESS) { | ||||
|             uint256 balanceBefore = weth.balanceOf(address(this)); | ||||
|             IERC20Bridge(providerAddress).bridgeTransferFrom( | ||||
|                 address(weth), | ||||
|                 address(0), | ||||
|                 address(this), | ||||
|                 minBuyAmount, | ||||
|                 "" | ||||
|             ); | ||||
|             boughtAmount = weth.balanceOf(address(this)).safeSub(balanceBefore); | ||||
|             // Unwrap wETH and send ETH to recipient. | ||||
|             weth.withdraw(boughtAmount); | ||||
|             recipient.transfer(boughtAmount); | ||||
|         } else { | ||||
|             uint256 balanceBefore = IERC20TokenV06(makerToken).balanceOf(recipient); | ||||
|             IERC20Bridge(providerAddress).bridgeTransferFrom( | ||||
|                 makerToken, | ||||
|                 address(0), | ||||
|                 recipient, | ||||
|                 minBuyAmount, | ||||
|                 "" | ||||
|             ); | ||||
|             boughtAmount = IERC20TokenV06(makerToken).balanceOf(recipient).safeSub(balanceBefore); | ||||
|         } | ||||
|         if (boughtAmount < minBuyAmount) { | ||||
|             LibLiquidityProviderRichErrors.LiquidityProviderIncompleteSellError( | ||||
|                 providerAddress, | ||||
|                 makerToken, | ||||
|                 takerToken, | ||||
|                 sellAmount, | ||||
|                 boughtAmount, | ||||
|                 minBuyAmount | ||||
|             ).rrevert(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// @dev Sets address of the liquidity provider for a market given | ||||
|     ///      (xAsset, yAsset). | ||||
|     /// @param xAsset First asset managed by the liquidity provider. | ||||
|     /// @param yAsset Second asset managed by the liquidity provider. | ||||
|     /// @param providerAddress Address of the liquidity provider. | ||||
|     function setLiquidityProviderForMarket( | ||||
|         address xAsset, | ||||
|         address yAsset, | ||||
|         address providerAddress | ||||
|     ) | ||||
|         external | ||||
|         override | ||||
|         onlyOwner | ||||
|     { | ||||
|         LibLiquidityProviderStorage.getStorage() | ||||
|             .addressBook[xAsset][yAsset] = providerAddress; | ||||
|         LibLiquidityProviderStorage.getStorage() | ||||
|             .addressBook[yAsset][xAsset] = providerAddress; | ||||
|         emit LiquidityProviderForMarketUpdated( | ||||
|             xAsset, | ||||
|             yAsset, | ||||
|             providerAddress | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /// @dev Returns the address of the liquidity provider for a market given | ||||
|     ///     (xAsset, yAsset), or reverts if pool does not exist. | ||||
|     /// @param xAsset First asset managed by the liquidity provider. | ||||
|     /// @param yAsset Second asset managed by the liquidity provider. | ||||
|     /// @return providerAddress Address of the liquidity provider. | ||||
|     function getLiquidityProviderForMarket( | ||||
|         address xAsset, | ||||
|         address yAsset | ||||
|     ) | ||||
|         public | ||||
|         view | ||||
|         override | ||||
|         returns (address providerAddress) | ||||
|     { | ||||
|         if (xAsset == ETH_TOKEN_ADDRESS) { | ||||
|             providerAddress = LibLiquidityProviderStorage.getStorage() | ||||
|                 .addressBook[address(weth)][yAsset]; | ||||
|         } else if (yAsset == ETH_TOKEN_ADDRESS) { | ||||
|             providerAddress = LibLiquidityProviderStorage.getStorage() | ||||
|                 .addressBook[xAsset][address(weth)]; | ||||
|         } else { | ||||
|             providerAddress = LibLiquidityProviderStorage.getStorage() | ||||
|                 .addressBook[xAsset][yAsset]; | ||||
|         } | ||||
|         if (providerAddress == address(0)) { | ||||
|             LibLiquidityProviderRichErrors.NoLiquidityProviderForMarketError( | ||||
|                 xAsset, | ||||
|                 yAsset | ||||
|             ).rrevert(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -184,7 +184,7 @@ contract MetaTransactionsFeature is | ||||
|  | ||||
|     /// @dev Execute a meta-transaction via `sender`. Privileged variant. | ||||
|     ///      Only callable from within. | ||||
|     /// @param sender Who is executing the meta-transaction.. | ||||
|     /// @param sender Who is executing the meta-transaction. | ||||
|     /// @param mtx The meta-transaction. | ||||
|     /// @param signature The signature by `mtx.signer`. | ||||
|     /// @return returnResult The ABI-encoded result of the underlying call. | ||||
| @@ -454,7 +454,7 @@ contract MetaTransactionsFeature is | ||||
|     } | ||||
|  | ||||
|     /// @dev Make an arbitrary internal, meta-transaction call. | ||||
|     ///      Warning: Do not let unadulerated `callData` into this function. | ||||
|     ///      Warning: Do not let unadulterated `callData` into this function. | ||||
|     function _callSelf(bytes32 hash, bytes memory callData, uint256 value) | ||||
|         private | ||||
|         returns (bytes memory returnResult) | ||||
|   | ||||
| @@ -0,0 +1,45 @@ | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "./LibStorage.sol"; | ||||
|  | ||||
|  | ||||
| /// @dev Storage helpers for `LiquidityProviderFeature`. | ||||
| library LibLiquidityProviderStorage { | ||||
|  | ||||
|     /// @dev Storage bucket for this feature. | ||||
|     struct Storage { | ||||
|         // Mapping of taker token -> maker token -> liquidity provider address | ||||
|         // Note that addressBook[x][y] == addressBook[y][x] will always hold. | ||||
|         mapping (address => mapping (address => address)) addressBook; | ||||
|     } | ||||
|  | ||||
|     /// @dev Get the storage bucket for this contract. | ||||
|     function getStorage() internal pure returns (Storage storage stor) { | ||||
|         uint256 storageSlot = LibStorage.getStorageSlot( | ||||
|             LibStorage.StorageId.LiquidityProvider | ||||
|         ); | ||||
|         // Dip into assembly to change the slot pointed to by the local | ||||
|         // variable `stor`. | ||||
|         // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries | ||||
|         assembly { stor_slot := storageSlot } | ||||
|     } | ||||
| } | ||||
| @@ -36,7 +36,8 @@ library LibStorage { | ||||
|         TokenSpender, | ||||
|         TransformERC20, | ||||
|         MetaTransactions, | ||||
|         ReentrancyGuard | ||||
|         ReentrancyGuard, | ||||
|         LiquidityProvider | ||||
|     } | ||||
|  | ||||
|     /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import "./mixins/MixinKyber.sol"; | ||||
| import "./mixins/MixinMooniswap.sol"; | ||||
| import "./mixins/MixinMStable.sol"; | ||||
| import "./mixins/MixinOasis.sol"; | ||||
| import "./mixins/MixinShell.sol"; | ||||
| import "./mixins/MixinUniswap.sol"; | ||||
| import "./mixins/MixinUniswapV2.sol"; | ||||
| import "./mixins/MixinZeroExBridge.sol"; | ||||
| @@ -38,6 +39,7 @@ contract BridgeAdapter is | ||||
|     MixinMooniswap, | ||||
|     MixinMStable, | ||||
|     MixinOasis, | ||||
|     MixinShell, | ||||
|     MixinUniswap, | ||||
|     MixinUniswapV2, | ||||
|     MixinZeroExBridge | ||||
| @@ -49,6 +51,7 @@ contract BridgeAdapter is | ||||
|     address private immutable MOONISWAP_BRIDGE_ADDRESS; | ||||
|     address private immutable MSTABLE_BRIDGE_ADDRESS; | ||||
|     address private immutable OASIS_BRIDGE_ADDRESS; | ||||
|     address private immutable SHELL_BRIDGE_ADDRESS; | ||||
|     address private immutable UNISWAP_BRIDGE_ADDRESS; | ||||
|     address private immutable UNISWAP_V2_BRIDGE_ADDRESS; | ||||
|  | ||||
| @@ -76,6 +79,7 @@ contract BridgeAdapter is | ||||
|         MixinMooniswap(addresses) | ||||
|         MixinMStable(addresses) | ||||
|         MixinOasis(addresses) | ||||
|         MixinShell(addresses) | ||||
|         MixinUniswap(addresses) | ||||
|         MixinUniswapV2(addresses) | ||||
|         MixinZeroExBridge() | ||||
| @@ -86,6 +90,7 @@ contract BridgeAdapter is | ||||
|         MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge; | ||||
|         MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge; | ||||
|         OASIS_BRIDGE_ADDRESS = addresses.oasisBridge; | ||||
|         SHELL_BRIDGE_ADDRESS = addresses.shellBridge; | ||||
|         UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge; | ||||
|         UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge; | ||||
|     } | ||||
| @@ -159,6 +164,12 @@ contract BridgeAdapter is | ||||
|                 sellAmount, | ||||
|                 bridgeData | ||||
|             ); | ||||
|         } else if (bridgeAddress == SHELL_BRIDGE_ADDRESS) { | ||||
|             boughtAmount = _tradeShell( | ||||
|                 buyToken, | ||||
|                 sellAmount, | ||||
|                 bridgeData | ||||
|             ); | ||||
|         } else { | ||||
|             boughtAmount = _tradeZeroExBridge( | ||||
|                 bridgeAddress, | ||||
|   | ||||
| @@ -29,6 +29,7 @@ contract MixinAdapterAddresses | ||||
|         address mooniswapBridge; | ||||
|         address mStableBridge; | ||||
|         address oasisBridge; | ||||
|         address shellBridge; | ||||
|         address uniswapBridge; | ||||
|         address uniswapV2Bridge; | ||||
|         // Exchanges | ||||
| @@ -37,6 +38,7 @@ contract MixinAdapterAddresses | ||||
|         address uniswapV2Router; | ||||
|         address uniswapExchangeFactory; | ||||
|         address mStable; | ||||
|         address shell; | ||||
|         // Other | ||||
|         address weth; | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
|  | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; | ||||
| import "./MixinAdapterAddresses.sol"; | ||||
|  | ||||
| interface IShell { | ||||
|  | ||||
|     function originSwap( | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 fromAmount, | ||||
|         uint256 minTargetAmount, | ||||
|         uint256 deadline | ||||
|     ) | ||||
|         external | ||||
|         returns (uint256 toAmount); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| contract MixinShell is | ||||
|     MixinAdapterAddresses | ||||
| { | ||||
|     using LibERC20TokenV06 for IERC20TokenV06; | ||||
|  | ||||
|     /// @dev Mainnet address of the `Shell` contract. | ||||
|     IShell private immutable SHELL; | ||||
|  | ||||
|     constructor(AdapterAddresses memory addresses) | ||||
|         public | ||||
|     { | ||||
|         SHELL = IShell(addresses.shell); | ||||
|     } | ||||
|  | ||||
|     function _tradeShell( | ||||
|         IERC20TokenV06 buyToken, | ||||
|         uint256 sellAmount, | ||||
|         bytes memory bridgeData | ||||
|     ) | ||||
|         internal | ||||
|         returns (uint256 boughtAmount) | ||||
|     { | ||||
|         (address fromTokenAddress) = abi.decode(bridgeData, (address)); | ||||
|  | ||||
|         // Grant the Shell contract an allowance to sell the first token. | ||||
|         IERC20TokenV06(fromTokenAddress).approveIfBelow( | ||||
|             address(SHELL), | ||||
|             sellAmount | ||||
|         ); | ||||
|  | ||||
|         uint256 buyAmount = SHELL.originSwap( | ||||
|             fromTokenAddress, | ||||
|             address(buyToken), | ||||
|              // Sell all tokens we hold. | ||||
|             sellAmount, | ||||
|              // Minimum buy amount. | ||||
|             1, | ||||
|             // deadline | ||||
|             block.timestamp + 1 | ||||
|         ); | ||||
|         return buyAmount; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										69
									
								
								contracts/zero-ex/contracts/test/TestBridge.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								contracts/zero-ex/contracts/test/TestBridge.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /* | ||||
|  | ||||
|   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.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; | ||||
| import "../src/vendor/v3/IERC20Bridge.sol"; | ||||
|  | ||||
|  | ||||
| contract TestBridge is | ||||
|     IERC20Bridge | ||||
| { | ||||
|     IERC20TokenV06 public immutable xAsset; | ||||
|     IERC20TokenV06 public immutable yAsset; | ||||
|  | ||||
|     constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_) | ||||
|         public | ||||
|     { | ||||
|         xAsset = xAsset_; | ||||
|         yAsset = yAsset_; | ||||
|     } | ||||
|  | ||||
|     /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. | ||||
|     /// @param tokenAddress The address of the ERC20 token to transfer. | ||||
|     /// @param from Address to transfer asset from. | ||||
|     /// @param to Address to transfer asset to. | ||||
|     /// @param amount Amount of asset to transfer. | ||||
|     /// @param bridgeData Arbitrary asset data needed by the bridge contract. | ||||
|     /// @return success The magic bytes `0xdc1600f3` if successful. | ||||
|     function bridgeTransferFrom( | ||||
|         address tokenAddress, | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 amount, | ||||
|         bytes calldata bridgeData | ||||
|     ) | ||||
|         external | ||||
|         override | ||||
|         returns (bytes4 success) | ||||
|     { | ||||
|         IERC20TokenV06 takerToken = tokenAddress == address(xAsset) ? yAsset : xAsset; | ||||
|         uint256 takerTokenBalance = takerToken.balanceOf(address(this)); | ||||
|         emit ERC20BridgeTransfer( | ||||
|             address(takerToken), | ||||
|             tokenAddress, | ||||
|             takerTokenBalance, | ||||
|             amount, | ||||
|             from, | ||||
|             to | ||||
|         ); | ||||
|         return 0xdecaf000; | ||||
|     } | ||||
| } | ||||
| @@ -39,9 +39,9 @@ | ||||
|         "publish:private": "yarn build && gitpkg publish" | ||||
|     }, | ||||
|     "config": { | ||||
|         "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter", | ||||
|         "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature", | ||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", | ||||
|         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" | ||||
|         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" | ||||
|     }, | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -55,6 +55,7 @@ | ||||
|     "devDependencies": { | ||||
|         "@0x/abi-gen": "^5.3.1", | ||||
|         "@0x/contracts-gen": "^2.0.10", | ||||
|         "@0x/contracts-erc20": "^3.2.1", | ||||
|         "@0x/contracts-test-utils": "^5.3.4", | ||||
|         "@0x/dev-utils": "^3.3.0", | ||||
|         "@0x/order-utils": "^10.3.0", | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import * as ISimpleFunctionRegistryFeature from '../generated-artifacts/ISimpleF | ||||
| import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json'; | ||||
| import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json'; | ||||
| import * as IZeroEx from '../generated-artifacts/IZeroEx.json'; | ||||
| import * as LiquidityProviderFeature from '../generated-artifacts/LiquidityProviderFeature.json'; | ||||
| import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json'; | ||||
| import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json'; | ||||
| import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; | ||||
| @@ -52,4 +53,5 @@ export const artifacts = { | ||||
|     MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, | ||||
|     LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, | ||||
|     BridgeAdapter: BridgeAdapter as ContractArtifact, | ||||
|     LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, | ||||
| }; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ export * from '../generated-wrappers/i_token_spender_feature'; | ||||
| export * from '../generated-wrappers/i_transform_erc20_feature'; | ||||
| export * from '../generated-wrappers/i_zero_ex'; | ||||
| export * from '../generated-wrappers/initial_migration'; | ||||
| export * from '../generated-wrappers/liquidity_provider_feature'; | ||||
| export * from '../generated-wrappers/log_metadata_transformer'; | ||||
| export * from '../generated-wrappers/meta_transactions_feature'; | ||||
| export * from '../generated-wrappers/ownable_feature'; | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json'; | ||||
| import * as IFeature from '../test/generated-artifacts/IFeature.json'; | ||||
| import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; | ||||
| import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; | ||||
| import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; | ||||
| import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; | ||||
| import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; | ||||
| import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json'; | ||||
| @@ -37,6 +38,8 @@ import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json'; | ||||
| import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; | ||||
| import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; | ||||
| import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json'; | ||||
| import * as LibLiquidityProviderRichErrors from '../test/generated-artifacts/LibLiquidityProviderRichErrors.json'; | ||||
| import * as LibLiquidityProviderStorage from '../test/generated-artifacts/LibLiquidityProviderStorage.json'; | ||||
| import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json'; | ||||
| import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; | ||||
| import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; | ||||
| @@ -55,6 +58,7 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe | ||||
| import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; | ||||
| import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; | ||||
| import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; | ||||
| import * as LiquidityProviderFeature from '../test/generated-artifacts/LiquidityProviderFeature.json'; | ||||
| import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; | ||||
| import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; | ||||
| import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json'; | ||||
| @@ -64,6 +68,7 @@ import * as MixinKyber from '../test/generated-artifacts/MixinKyber.json'; | ||||
| import * as MixinMooniswap from '../test/generated-artifacts/MixinMooniswap.json'; | ||||
| import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json'; | ||||
| import * as MixinOasis from '../test/generated-artifacts/MixinOasis.json'; | ||||
| import * as MixinShell from '../test/generated-artifacts/MixinShell.json'; | ||||
| import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json'; | ||||
| import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json'; | ||||
| import * as MixinZeroExBridge from '../test/generated-artifacts/MixinZeroExBridge.json'; | ||||
| @@ -71,6 +76,7 @@ import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json | ||||
| import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; | ||||
| import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json'; | ||||
| import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; | ||||
| import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; | ||||
| import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; | ||||
| import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; | ||||
| import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json'; | ||||
| @@ -104,6 +110,7 @@ export const artifacts = { | ||||
|     IZeroEx: IZeroEx as ContractArtifact, | ||||
|     ZeroEx: ZeroEx as ContractArtifact, | ||||
|     LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, | ||||
|     LibLiquidityProviderRichErrors: LibLiquidityProviderRichErrors as ContractArtifact, | ||||
|     LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, | ||||
|     LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, | ||||
|     LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, | ||||
| @@ -120,6 +127,7 @@ export const artifacts = { | ||||
|     BootstrapFeature: BootstrapFeature as ContractArtifact, | ||||
|     IBootstrapFeature: IBootstrapFeature as ContractArtifact, | ||||
|     IFeature: IFeature as ContractArtifact, | ||||
|     ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, | ||||
|     IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, | ||||
|     IOwnableFeature: IOwnableFeature as ContractArtifact, | ||||
|     ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact, | ||||
| @@ -127,6 +135,7 @@ export const artifacts = { | ||||
|     ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, | ||||
|     ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, | ||||
|     IUniswapFeature: IUniswapFeature as ContractArtifact, | ||||
|     LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, | ||||
|     MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, | ||||
|     OwnableFeature: OwnableFeature as ContractArtifact, | ||||
|     SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact, | ||||
| @@ -142,6 +151,7 @@ export const artifacts = { | ||||
|     InitialMigration: InitialMigration as ContractArtifact, | ||||
|     LibBootstrap: LibBootstrap as ContractArtifact, | ||||
|     LibMigrate: LibMigrate as ContractArtifact, | ||||
|     LibLiquidityProviderStorage: LibLiquidityProviderStorage as ContractArtifact, | ||||
|     LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, | ||||
|     LibOwnableStorage: LibOwnableStorage as ContractArtifact, | ||||
|     LibProxyStorage: LibProxyStorage as ContractArtifact, | ||||
| @@ -167,6 +177,7 @@ export const artifacts = { | ||||
|     MixinMStable: MixinMStable as ContractArtifact, | ||||
|     MixinMooniswap: MixinMooniswap as ContractArtifact, | ||||
|     MixinOasis: MixinOasis as ContractArtifact, | ||||
|     MixinShell: MixinShell as ContractArtifact, | ||||
|     MixinUniswap: MixinUniswap as ContractArtifact, | ||||
|     MixinUniswapV2: MixinUniswapV2 as ContractArtifact, | ||||
|     MixinZeroExBridge: MixinZeroExBridge as ContractArtifact, | ||||
| @@ -174,6 +185,7 @@ export const artifacts = { | ||||
|     IExchange: IExchange as ContractArtifact, | ||||
|     IGasToken: IGasToken as ContractArtifact, | ||||
|     ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, | ||||
|     TestBridge: TestBridge as ContractArtifact, | ||||
|     TestCallTarget: TestCallTarget as ContractArtifact, | ||||
|     TestDelegateCaller: TestDelegateCaller as ContractArtifact, | ||||
|     TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, | ||||
|   | ||||
							
								
								
									
										250
									
								
								contracts/zero-ex/test/features/liquidity_provider_test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								contracts/zero-ex/test/features/liquidity_provider_test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,250 @@ | ||||
| import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; | ||||
| import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils'; | ||||
| import { BigNumber, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils'; | ||||
|  | ||||
| import { | ||||
|     IOwnableFeatureContract, | ||||
|     IZeroExContract, | ||||
|     LiquidityProviderFeatureContract, | ||||
|     TokenSpenderFeatureContract, | ||||
| } from '../../src/wrappers'; | ||||
| import { artifacts } from '../artifacts'; | ||||
| import { abis } from '../utils/abis'; | ||||
| import { fullMigrateAsync } from '../utils/migration'; | ||||
| import { IERC20BridgeEvents, TestBridgeContract, TestWethContract } from '../wrappers'; | ||||
|  | ||||
| blockchainTests('LiquidityProvider feature', env => { | ||||
|     let zeroEx: IZeroExContract; | ||||
|     let feature: LiquidityProviderFeatureContract; | ||||
|     let token: DummyERC20TokenContract; | ||||
|     let weth: TestWethContract; | ||||
|     let owner: string; | ||||
|     let taker: string; | ||||
|  | ||||
|     before(async () => { | ||||
|         [owner, taker] = await env.getAccountAddressesAsync(); | ||||
|         zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { | ||||
|             tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync( | ||||
|                 artifacts.TestTokenSpender, | ||||
|                 env.provider, | ||||
|                 env.txDefaults, | ||||
|                 artifacts, | ||||
|             )).address, | ||||
|         }); | ||||
|         const tokenSpender = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); | ||||
|         const allowanceTarget = await tokenSpender.getAllowanceTarget().callAsync(); | ||||
|  | ||||
|         token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( | ||||
|             erc20Artifacts.DummyERC20Token, | ||||
|             env.provider, | ||||
|             env.txDefaults, | ||||
|             erc20Artifacts, | ||||
|             constants.DUMMY_TOKEN_NAME, | ||||
|             constants.DUMMY_TOKEN_SYMBOL, | ||||
|             constants.DUMMY_TOKEN_DECIMALS, | ||||
|             constants.DUMMY_TOKEN_TOTAL_SUPPLY, | ||||
|         ); | ||||
|         await token.setBalance(taker, constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync(); | ||||
|         weth = await TestWethContract.deployFrom0xArtifactAsync( | ||||
|             artifacts.TestWeth, | ||||
|             env.provider, | ||||
|             env.txDefaults, | ||||
|             artifacts, | ||||
|         ); | ||||
|         await token | ||||
|             .approve(allowanceTarget, constants.INITIAL_ERC20_ALLOWANCE) | ||||
|             .awaitTransactionSuccessAsync({ from: taker }); | ||||
|  | ||||
|         feature = new LiquidityProviderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); | ||||
|         const featureImpl = await LiquidityProviderFeatureContract.deployFrom0xArtifactAsync( | ||||
|             artifacts.LiquidityProviderFeature, | ||||
|             env.provider, | ||||
|             env.txDefaults, | ||||
|             artifacts, | ||||
|             weth.address, | ||||
|         ); | ||||
|         await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis) | ||||
|             .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) | ||||
|             .awaitTransactionSuccessAsync(); | ||||
|     }); | ||||
|     describe('Registry', () => { | ||||
|         it('`getLiquidityProviderForMarket` reverts if address is not set', async () => { | ||||
|             const [xAsset, yAsset] = [randomAddress(), randomAddress()]; | ||||
|             let tx = feature.getLiquidityProviderForMarket(xAsset, yAsset).awaitTransactionSuccessAsync(); | ||||
|             expect(tx).to.revertWith( | ||||
|                 new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset), | ||||
|             ); | ||||
|             tx = feature.getLiquidityProviderForMarket(yAsset, xAsset).awaitTransactionSuccessAsync(); | ||||
|             return expect(tx).to.revertWith( | ||||
|                 new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(yAsset, xAsset), | ||||
|             ); | ||||
|         }); | ||||
|         it('can set/get a liquidity provider address for a given market', async () => { | ||||
|             const expectedAddress = randomAddress(); | ||||
|             await feature | ||||
|                 .setLiquidityProviderForMarket(token.address, weth.address, expectedAddress) | ||||
|                 .awaitTransactionSuccessAsync(); | ||||
|             let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync(); | ||||
|             expect(actualAddress).to.equal(expectedAddress); | ||||
|             actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync(); | ||||
|             expect(actualAddress).to.equal(expectedAddress); | ||||
|         }); | ||||
|         it('can update a liquidity provider address for a given market', async () => { | ||||
|             const expectedAddress = randomAddress(); | ||||
|             await feature | ||||
|                 .setLiquidityProviderForMarket(token.address, weth.address, expectedAddress) | ||||
|                 .awaitTransactionSuccessAsync(); | ||||
|             let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync(); | ||||
|             expect(actualAddress).to.equal(expectedAddress); | ||||
|             actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync(); | ||||
|             expect(actualAddress).to.equal(expectedAddress); | ||||
|         }); | ||||
|         it('can effectively remove a liquidity provider for a market by setting the address to 0', async () => { | ||||
|             await feature | ||||
|                 .setLiquidityProviderForMarket(token.address, weth.address, constants.NULL_ADDRESS) | ||||
|                 .awaitTransactionSuccessAsync(); | ||||
|             const tx = feature | ||||
|                 .getLiquidityProviderForMarket(token.address, weth.address) | ||||
|                 .awaitTransactionSuccessAsync(); | ||||
|             return expect(tx).to.revertWith( | ||||
|                 new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(token.address, weth.address), | ||||
|             ); | ||||
|         }); | ||||
|         it('reverts if non-owner attempts to set an address', async () => { | ||||
|             const tx = feature | ||||
|                 .setLiquidityProviderForMarket(randomAddress(), randomAddress(), randomAddress()) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker }); | ||||
|             return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(taker, owner)); | ||||
|         }); | ||||
|     }); | ||||
|     blockchainTests.resets('Swap', () => { | ||||
|         let liquidityProvider: TestBridgeContract; | ||||
|         const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; | ||||
|  | ||||
|         before(async () => { | ||||
|             liquidityProvider = await TestBridgeContract.deployFrom0xArtifactAsync( | ||||
|                 artifacts.TestBridge, | ||||
|                 env.provider, | ||||
|                 env.txDefaults, | ||||
|                 artifacts, | ||||
|                 token.address, | ||||
|                 weth.address, | ||||
|             ); | ||||
|             await feature | ||||
|                 .setLiquidityProviderForMarket(token.address, weth.address, liquidityProvider.address) | ||||
|                 .awaitTransactionSuccessAsync(); | ||||
|         }); | ||||
|         it('Cannot execute a swap for a market without a liquidity provider set', async () => { | ||||
|             const [xAsset, yAsset] = [randomAddress(), randomAddress()]; | ||||
|             const tx = feature | ||||
|                 .sellToLiquidityProvider( | ||||
|                     xAsset, | ||||
|                     yAsset, | ||||
|                     constants.NULL_ADDRESS, | ||||
|                     constants.ONE_ETHER, | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker }); | ||||
|             return expect(tx).to.revertWith( | ||||
|                 new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset), | ||||
|             ); | ||||
|         }); | ||||
|         it('Successfully executes an ERC20-ERC20 swap', async () => { | ||||
|             const tx = await feature | ||||
|                 .sellToLiquidityProvider( | ||||
|                     weth.address, | ||||
|                     token.address, | ||||
|                     constants.NULL_ADDRESS, | ||||
|                     constants.ONE_ETHER, | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker }); | ||||
|             verifyEventsFromLogs( | ||||
|                 tx.logs, | ||||
|                 [ | ||||
|                     { | ||||
|                         inputToken: token.address, | ||||
|                         outputToken: weth.address, | ||||
|                         inputTokenAmount: constants.ONE_ETHER, | ||||
|                         outputTokenAmount: constants.ZERO_AMOUNT, | ||||
|                         from: constants.NULL_ADDRESS, | ||||
|                         to: taker, | ||||
|                     }, | ||||
|                 ], | ||||
|                 IERC20BridgeEvents.ERC20BridgeTransfer, | ||||
|             ); | ||||
|         }); | ||||
|         it('Reverts if cannot fulfill the minimum buy amount', async () => { | ||||
|             const minBuyAmount = new BigNumber(1); | ||||
|             const tx = feature | ||||
|                 .sellToLiquidityProvider( | ||||
|                     weth.address, | ||||
|                     token.address, | ||||
|                     constants.NULL_ADDRESS, | ||||
|                     constants.ONE_ETHER, | ||||
|                     minBuyAmount, | ||||
|                 ) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker }); | ||||
|             return expect(tx).to.revertWith( | ||||
|                 new ZeroExRevertErrors.LiquidityProvider.LiquidityProviderIncompleteSellError( | ||||
|                     liquidityProvider.address, | ||||
|                     weth.address, | ||||
|                     token.address, | ||||
|                     constants.ONE_ETHER, | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     minBuyAmount, | ||||
|                 ), | ||||
|             ); | ||||
|         }); | ||||
|         it('Successfully executes an ETH-ERC20 swap', async () => { | ||||
|             const tx = await feature | ||||
|                 .sellToLiquidityProvider( | ||||
|                     token.address, | ||||
|                     ETH_TOKEN_ADDRESS, | ||||
|                     constants.NULL_ADDRESS, | ||||
|                     constants.ONE_ETHER, | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker, value: constants.ONE_ETHER }); | ||||
|             verifyEventsFromLogs( | ||||
|                 tx.logs, | ||||
|                 [ | ||||
|                     { | ||||
|                         inputToken: weth.address, | ||||
|                         outputToken: token.address, | ||||
|                         inputTokenAmount: constants.ONE_ETHER, | ||||
|                         outputTokenAmount: constants.ZERO_AMOUNT, | ||||
|                         from: constants.NULL_ADDRESS, | ||||
|                         to: taker, | ||||
|                     }, | ||||
|                 ], | ||||
|                 IERC20BridgeEvents.ERC20BridgeTransfer, | ||||
|             ); | ||||
|         }); | ||||
|         it('Successfully executes an ERC20-ETH swap', async () => { | ||||
|             const tx = await feature | ||||
|                 .sellToLiquidityProvider( | ||||
|                     ETH_TOKEN_ADDRESS, | ||||
|                     token.address, | ||||
|                     constants.NULL_ADDRESS, | ||||
|                     constants.ONE_ETHER, | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ) | ||||
|                 .awaitTransactionSuccessAsync({ from: taker }); | ||||
|             verifyEventsFromLogs( | ||||
|                 tx.logs, | ||||
|                 [ | ||||
|                     { | ||||
|                         inputToken: token.address, | ||||
|                         outputToken: weth.address, | ||||
|                         inputTokenAmount: constants.ONE_ETHER, | ||||
|                         outputTokenAmount: constants.ZERO_AMOUNT, | ||||
|                         from: constants.NULL_ADDRESS, | ||||
|                         to: zeroEx.address, | ||||
|                     }, | ||||
|                 ], | ||||
|                 IERC20BridgeEvents.ERC20BridgeTransfer, | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -73,6 +73,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { | ||||
|                 uniswapExchangeFactory: NULL_ADDRESS, | ||||
|                 mStable: NULL_ADDRESS, | ||||
|                 weth: NULL_ADDRESS, | ||||
|                 shellBridge: NULL_ADDRESS, | ||||
|                 shell: NULL_ADDRESS, | ||||
|             }, | ||||
|         ); | ||||
|         transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( | ||||
|   | ||||
| @@ -22,6 +22,7 @@ export * from '../test/generated-wrappers/i_exchange'; | ||||
| export * from '../test/generated-wrappers/i_feature'; | ||||
| export * from '../test/generated-wrappers/i_flash_wallet'; | ||||
| export * from '../test/generated-wrappers/i_gas_token'; | ||||
| export * from '../test/generated-wrappers/i_liquidity_provider_feature'; | ||||
| export * from '../test/generated-wrappers/i_meta_transactions_feature'; | ||||
| export * from '../test/generated-wrappers/i_ownable_feature'; | ||||
| export * from '../test/generated-wrappers/i_signature_validator_feature'; | ||||
| @@ -35,6 +36,8 @@ export * from '../test/generated-wrappers/initial_migration'; | ||||
| export * from '../test/generated-wrappers/lib_bootstrap'; | ||||
| export * from '../test/generated-wrappers/lib_common_rich_errors'; | ||||
| export * from '../test/generated-wrappers/lib_erc20_transformer'; | ||||
| export * from '../test/generated-wrappers/lib_liquidity_provider_rich_errors'; | ||||
| export * from '../test/generated-wrappers/lib_liquidity_provider_storage'; | ||||
| export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors'; | ||||
| export * from '../test/generated-wrappers/lib_meta_transactions_storage'; | ||||
| export * from '../test/generated-wrappers/lib_migrate'; | ||||
| @@ -53,6 +56,7 @@ export * from '../test/generated-wrappers/lib_token_spender_storage'; | ||||
| export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; | ||||
| export * from '../test/generated-wrappers/lib_transform_erc20_storage'; | ||||
| export * from '../test/generated-wrappers/lib_wallet_rich_errors'; | ||||
| export * from '../test/generated-wrappers/liquidity_provider_feature'; | ||||
| export * from '../test/generated-wrappers/log_metadata_transformer'; | ||||
| export * from '../test/generated-wrappers/meta_transactions_feature'; | ||||
| export * from '../test/generated-wrappers/mixin_adapter_addresses'; | ||||
| @@ -62,6 +66,7 @@ export * from '../test/generated-wrappers/mixin_kyber'; | ||||
| export * from '../test/generated-wrappers/mixin_m_stable'; | ||||
| export * from '../test/generated-wrappers/mixin_mooniswap'; | ||||
| export * from '../test/generated-wrappers/mixin_oasis'; | ||||
| export * from '../test/generated-wrappers/mixin_shell'; | ||||
| export * from '../test/generated-wrappers/mixin_uniswap'; | ||||
| export * from '../test/generated-wrappers/mixin_uniswap_v2'; | ||||
| export * from '../test/generated-wrappers/mixin_zero_ex_bridge'; | ||||
| @@ -69,6 +74,7 @@ export * from '../test/generated-wrappers/ownable_feature'; | ||||
| export * from '../test/generated-wrappers/pay_taker_transformer'; | ||||
| export * from '../test/generated-wrappers/signature_validator_feature'; | ||||
| export * from '../test/generated-wrappers/simple_function_registry_feature'; | ||||
| export * from '../test/generated-wrappers/test_bridge'; | ||||
| export * from '../test/generated-wrappers/test_call_target'; | ||||
| export * from '../test/generated-wrappers/test_delegate_caller'; | ||||
| export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|         "generated-artifacts/ITransformERC20Feature.json", | ||||
|         "generated-artifacts/IZeroEx.json", | ||||
|         "generated-artifacts/InitialMigration.json", | ||||
|         "generated-artifacts/LiquidityProviderFeature.json", | ||||
|         "generated-artifacts/LogMetadataTransformer.json", | ||||
|         "generated-artifacts/MetaTransactionsFeature.json", | ||||
|         "generated-artifacts/OwnableFeature.json", | ||||
| @@ -45,6 +46,7 @@ | ||||
|         "test/generated-artifacts/IFeature.json", | ||||
|         "test/generated-artifacts/IFlashWallet.json", | ||||
|         "test/generated-artifacts/IGasToken.json", | ||||
|         "test/generated-artifacts/ILiquidityProviderFeature.json", | ||||
|         "test/generated-artifacts/IMetaTransactionsFeature.json", | ||||
|         "test/generated-artifacts/IOwnableFeature.json", | ||||
|         "test/generated-artifacts/ISignatureValidatorFeature.json", | ||||
| @@ -58,6 +60,8 @@ | ||||
|         "test/generated-artifacts/LibBootstrap.json", | ||||
|         "test/generated-artifacts/LibCommonRichErrors.json", | ||||
|         "test/generated-artifacts/LibERC20Transformer.json", | ||||
|         "test/generated-artifacts/LibLiquidityProviderRichErrors.json", | ||||
|         "test/generated-artifacts/LibLiquidityProviderStorage.json", | ||||
|         "test/generated-artifacts/LibMetaTransactionsRichErrors.json", | ||||
|         "test/generated-artifacts/LibMetaTransactionsStorage.json", | ||||
|         "test/generated-artifacts/LibMigrate.json", | ||||
| @@ -76,6 +80,7 @@ | ||||
|         "test/generated-artifacts/LibTransformERC20RichErrors.json", | ||||
|         "test/generated-artifacts/LibTransformERC20Storage.json", | ||||
|         "test/generated-artifacts/LibWalletRichErrors.json", | ||||
|         "test/generated-artifacts/LiquidityProviderFeature.json", | ||||
|         "test/generated-artifacts/LogMetadataTransformer.json", | ||||
|         "test/generated-artifacts/MetaTransactionsFeature.json", | ||||
|         "test/generated-artifacts/MixinAdapterAddresses.json", | ||||
| @@ -85,6 +90,7 @@ | ||||
|         "test/generated-artifacts/MixinMStable.json", | ||||
|         "test/generated-artifacts/MixinMooniswap.json", | ||||
|         "test/generated-artifacts/MixinOasis.json", | ||||
|         "test/generated-artifacts/MixinShell.json", | ||||
|         "test/generated-artifacts/MixinUniswap.json", | ||||
|         "test/generated-artifacts/MixinUniswapV2.json", | ||||
|         "test/generated-artifacts/MixinZeroExBridge.json", | ||||
| @@ -92,6 +98,7 @@ | ||||
|         "test/generated-artifacts/PayTakerTransformer.json", | ||||
|         "test/generated-artifacts/SignatureValidatorFeature.json", | ||||
|         "test/generated-artifacts/SimpleFunctionRegistryFeature.json", | ||||
|         "test/generated-artifacts/TestBridge.json", | ||||
|         "test/generated-artifacts/TestCallTarget.json", | ||||
|         "test/generated-artifacts/TestDelegateCaller.json", | ||||
|         "test/generated-artifacts/TestFillQuoteTransformerBridge.json", | ||||
|   | ||||
| @@ -133,6 +133,22 @@ | ||||
|             { | ||||
|                 "note": "Respect max slippage in EP consumer", | ||||
|                 "pr": 2712 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Introduced Path class, exchangeProxyOverhead parameter", | ||||
|                 "pr": 2691 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added `Shell`", | ||||
|                 "pr": 2722 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Fix exchange proxy overhead gas being scaled by gas price", | ||||
|                 "pr": 2723 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Remove 0x-API swap/v0-specifc code from asset-swapper", | ||||
|                 "pr": 2725 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -78,16 +78,22 @@ contract ApproximateBuys { | ||||
|         for (uint256 i = 0; i < makerTokenAmounts.length; i++) { | ||||
|             for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) { | ||||
|                 // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER | ||||
|                 sellAmount = LibMath.getPartialAmountCeil( | ||||
|                 sellAmount = _safeGetPartialAmountCeil( | ||||
|                     makerTokenAmounts[i], | ||||
|                     buyAmount, | ||||
|                     sellAmount | ||||
|                 ); | ||||
|                 sellAmount = LibMath.getPartialAmountCeil( | ||||
|                 if (sellAmount == 0) { | ||||
|                     break; | ||||
|                 } | ||||
|                 sellAmount = _safeGetPartialAmountCeil( | ||||
|                     (ONE_HUNDED_PERCENT_BPS + APPROXIMATE_BUY_TARGET_EPSILON_BPS), | ||||
|                     ONE_HUNDED_PERCENT_BPS, | ||||
|                     sellAmount | ||||
|                 ); | ||||
|                 if (sellAmount == 0) { | ||||
|                     break; | ||||
|                 } | ||||
|                 uint256 _buyAmount = opts.getSellQuoteCallback( | ||||
|                     opts.takerTokenData, | ||||
|                     opts.makerTokenData, | ||||
| @@ -112,11 +118,26 @@ contract ApproximateBuys { | ||||
|             // We do our best to close in on the requested amount, but we can either over buy or under buy and exit | ||||
|             // if we hit a max iteration limit | ||||
|             // We scale the sell amount to get the approximate target | ||||
|             takerTokenAmounts[i] = LibMath.getPartialAmountCeil( | ||||
|             takerTokenAmounts[i] = _safeGetPartialAmountCeil( | ||||
|                 makerTokenAmounts[i], | ||||
|                 buyAmount, | ||||
|                 sellAmount | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function _safeGetPartialAmountCeil( | ||||
|         uint256 numerator, | ||||
|         uint256 denominator, | ||||
|         uint256 target | ||||
|     ) | ||||
|         internal | ||||
|         view | ||||
|         returns (uint256 partialAmount) | ||||
|     { | ||||
|         if (numerator == 0 || target == 0 || denominator == 0) return 0; | ||||
|         uint256 c = numerator * target; | ||||
|         if (c / numerator != target) return 0; | ||||
|         return (c + (denominator - 1)) / denominator; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import "./MultiBridgeSampler.sol"; | ||||
| import "./MStableSampler.sol"; | ||||
| import "./MooniswapSampler.sol"; | ||||
| import "./NativeOrderSampler.sol"; | ||||
| import "./ShellSampler.sol"; | ||||
| import "./SushiSwapSampler.sol"; | ||||
| import "./TwoHopSampler.sol"; | ||||
| import "./UniswapSampler.sol"; | ||||
| @@ -44,6 +45,7 @@ contract ERC20BridgeSampler is | ||||
|     MooniswapSampler, | ||||
|     MultiBridgeSampler, | ||||
|     NativeOrderSampler, | ||||
|     ShellSampler, | ||||
|     SushiSwapSampler, | ||||
|     TwoHopSampler, | ||||
|     UniswapSampler, | ||||
|   | ||||
							
								
								
									
										110
									
								
								packages/asset-swapper/contracts/src/ShellSampler.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								packages/asset-swapper/contracts/src/ShellSampler.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| /* | ||||
|  | ||||
|   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-utils/contracts/src/DeploymentConstants.sol"; | ||||
| import "./interfaces/IShell.sol"; | ||||
|  | ||||
| contract ShellSampler is | ||||
|     DeploymentConstants | ||||
| { | ||||
|     /// @dev Default gas limit for Shell calls. | ||||
|     uint256 constant private DEFAULT_CALL_GAS = 300e3; // 300k | ||||
|  | ||||
|     /// @dev Sample sell quotes from the Shell contract | ||||
|     /// @param takerToken Address of the taker token (what to sell). | ||||
|     /// @param makerToken Address of the maker token (what to buy). | ||||
|     /// @param takerTokenAmounts Taker token sell amount for each sample. | ||||
|     /// @return makerTokenAmounts Maker amounts bought at each taker token | ||||
|     ///         amount. | ||||
|     function sampleSellsFromShell( | ||||
|         address takerToken, | ||||
|         address makerToken, | ||||
|         uint256[] memory takerTokenAmounts | ||||
|     ) | ||||
|         public | ||||
|         view | ||||
|         returns (uint256[] memory makerTokenAmounts) | ||||
|     { | ||||
|         // Initialize array of maker token amounts. | ||||
|         uint256 numSamples = takerTokenAmounts.length; | ||||
|         makerTokenAmounts = new uint256[](numSamples); | ||||
|  | ||||
|         for (uint256 i = 0; i < numSamples; i++) { | ||||
|             (bool didSucceed, bytes memory resultData) = | ||||
|                 address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)( | ||||
|                     abi.encodeWithSelector( | ||||
|                         IShell(0).viewOriginSwap.selector, | ||||
|                         takerToken, | ||||
|                         makerToken, | ||||
|                         takerTokenAmounts[i] | ||||
|                     )); | ||||
|             uint256 buyAmount = 0; | ||||
|             if (didSucceed) { | ||||
|                 buyAmount = abi.decode(resultData, (uint256)); | ||||
|             } | ||||
|             // Exit early if the amount is too high for the source to serve | ||||
|             if (buyAmount == 0) { | ||||
|                 break; | ||||
|             } | ||||
|             makerTokenAmounts[i] = buyAmount; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// @dev Sample buy quotes from Shell contract | ||||
|     /// @param takerToken Address of the taker token (what to sell). | ||||
|     /// @param makerToken Address of the maker token (what to buy). | ||||
|     /// @param makerTokenAmounts Maker token buy amount for each sample. | ||||
|     /// @return takerTokenAmounts Taker amounts sold at each maker token | ||||
|     ///         amount. | ||||
|     function sampleBuysFromShell( | ||||
|         address takerToken, | ||||
|         address makerToken, | ||||
|         uint256[] memory makerTokenAmounts | ||||
|     ) | ||||
|         public | ||||
|         view | ||||
|         returns (uint256[] memory takerTokenAmounts) | ||||
|     { | ||||
|         // Initialize array of maker token amounts. | ||||
|         uint256 numSamples = makerTokenAmounts.length; | ||||
|         takerTokenAmounts = new uint256[](numSamples); | ||||
|  | ||||
|         for (uint256 i = 0; i < numSamples; i++) { | ||||
|             (bool didSucceed, bytes memory resultData) = | ||||
|                 address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)( | ||||
|                     abi.encodeWithSelector( | ||||
|                         IShell(0).viewTargetSwap.selector, | ||||
|                         takerToken, | ||||
|                         makerToken, | ||||
|                         makerTokenAmounts[i] | ||||
|                     )); | ||||
|             uint256 sellAmount = 0; | ||||
|             if (didSucceed) { | ||||
|                 sellAmount = abi.decode(resultData, (uint256)); | ||||
|             } | ||||
|             // Exit early if the amount is too high for the source to serve | ||||
|             if (sellAmount == 0) { | ||||
|                 break; | ||||
|             } | ||||
|             takerTokenAmounts[i] = sellAmount; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								packages/asset-swapper/contracts/src/interfaces/IShell.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								packages/asset-swapper/contracts/src/interfaces/IShell.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| /* | ||||
|  | ||||
|   Copyright 2020 ZeroEx Intl. | ||||
|  | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
|  | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.5.9; | ||||
|  | ||||
|  | ||||
| interface IShell { | ||||
|  | ||||
|     function viewOriginSwap ( | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 fromAmount | ||||
|     ) | ||||
|         external | ||||
|         view | ||||
|         returns (uint256 toAmount); | ||||
|  | ||||
|     function viewTargetSwap ( | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 toAmount | ||||
|     ) | ||||
|         external | ||||
|         view | ||||
|         returns (uint256 fromAmount); | ||||
| } | ||||
|  | ||||
| @@ -17,7 +17,7 @@ | ||||
|         "compile": "sol-compiler", | ||||
|         "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./test/generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude ./test/generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", | ||||
|         "lint-contracts": "#solhint -c .solhint.json contracts/**/**/**/**/*.sol", | ||||
|         "prettier": "prettier '**/*.{ts,tsx,json,md}' --config ../../.prettierrc  --ignore-path ../../.prettierignore", | ||||
|         "prettier": "prettier --write '**/*.{ts,tsx,json,md}' --config ../../.prettierrc  --ignore-path ../../.prettierignore", | ||||
|         "fix": "tslint --fix --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude ./test/generated-wrappers/**/* --exclude ./test/generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", | ||||
|         "test": "yarn run_mocha", | ||||
|         "rebuild_and_test": "run-s clean build test", | ||||
| @@ -38,7 +38,7 @@ | ||||
|     "config": { | ||||
|         "publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", | ||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", | ||||
|         "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberNetwork|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", | ||||
|         "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberNetwork|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", | ||||
|         "postpublish": { | ||||
|             "assets": [] | ||||
|         } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import { BigNumber, logUtils } from '@0x/utils'; | ||||
|  | ||||
| import { | ||||
|     ExchangeProxyContractOpts, | ||||
|     ExtensionContractType, | ||||
|     ForwarderExtensionContractOpts, | ||||
|     LogFunction, | ||||
|     OrderPrunerOpts, | ||||
|     OrderPrunerPermittedFeeTypes, | ||||
|     RfqtRequestOpts, | ||||
| @@ -89,6 +90,11 @@ const DEFAULT_RFQT_REQUEST_OPTS: Partial<RfqtRequestOpts> = { | ||||
|     makerEndpointMaxResponseTimeMs: 1000, | ||||
| }; | ||||
|  | ||||
| export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) => | ||||
|     logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`); | ||||
| export const DEFAULT_WARNING_LOGGER: LogFunction = (obj, msg) => | ||||
|     logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`); | ||||
|  | ||||
| export const constants = { | ||||
|     ETH_GAS_STATION_API_URL, | ||||
|     PROTOCOL_FEE_MULTIPLIER, | ||||
| @@ -113,4 +119,6 @@ export const constants = { | ||||
|     PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, | ||||
|     MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, | ||||
|     BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3', | ||||
|     DEFAULT_INFO_LOGGER, | ||||
|     DEFAULT_WARNING_LOGGER, | ||||
| }; | ||||
|   | ||||
| @@ -119,6 +119,7 @@ export { | ||||
|     SwapQuoterRfqtOpts, | ||||
| } from './types'; | ||||
| export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; | ||||
| export { SOURCE_FLAGS } from './utils/market_operation_utils/constants'; | ||||
| export { | ||||
|     Parameters, | ||||
|     SamplerContractCall, | ||||
| @@ -133,10 +134,10 @@ export { | ||||
|     CurveInfo, | ||||
|     DexSample, | ||||
|     ERC20BridgeSource, | ||||
|     ExchangeProxyOverhead, | ||||
|     FeeSchedule, | ||||
|     Fill, | ||||
|     FillData, | ||||
|     FillFlags, | ||||
|     GetMarketOrdersRfqtOpts, | ||||
|     KyberFillData, | ||||
|     LiquidityProviderFillData, | ||||
|   | ||||
| @@ -1,115 +0,0 @@ | ||||
| import { ContractAddresses } from '@0x/contract-addresses'; | ||||
| import { ExchangeContract } from '@0x/contract-wrappers'; | ||||
| import { providerUtils } from '@0x/utils'; | ||||
| import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { constants } from '../constants'; | ||||
| import { | ||||
|     CalldataInfo, | ||||
|     MarketOperation, | ||||
|     SwapQuote, | ||||
|     SwapQuoteConsumerBase, | ||||
|     SwapQuoteConsumerOpts, | ||||
|     SwapQuoteExecutionOpts, | ||||
|     SwapQuoteGetOutputOpts, | ||||
| } from '../types'; | ||||
| import { assert } from '../utils/assert'; | ||||
| import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; | ||||
|  | ||||
| export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|     public readonly provider: ZeroExProvider; | ||||
|     public readonly chainId: number; | ||||
|  | ||||
|     private readonly _exchangeContract: ExchangeContract; | ||||
|  | ||||
|     constructor( | ||||
|         supportedProvider: SupportedProvider, | ||||
|         public readonly contractAddresses: ContractAddresses, | ||||
|         options: Partial<SwapQuoteConsumerOpts> = {}, | ||||
|     ) { | ||||
|         const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); | ||||
|         assert.isNumber('chainId', chainId); | ||||
|         const provider = providerUtils.standardizeOrThrow(supportedProvider); | ||||
|         this.provider = provider; | ||||
|         this.chainId = chainId; | ||||
|         this._exchangeContract = new ExchangeContract(contractAddresses.exchange, supportedProvider); | ||||
|     } | ||||
|  | ||||
|     public async getCalldataOrThrowAsync( | ||||
|         quote: SwapQuote, | ||||
|         _opts: Partial<SwapQuoteGetOutputOpts> = {}, | ||||
|     ): Promise<CalldataInfo> { | ||||
|         assert.isValidSwapQuote('quote', quote); | ||||
|         const { orders } = quote; | ||||
|         const signatures = _.map(orders, o => o.signature); | ||||
|  | ||||
|         let calldataHexString; | ||||
|         if (quote.type === MarketOperation.Buy) { | ||||
|             calldataHexString = this._exchangeContract | ||||
|                 .marketBuyOrdersFillOrKill(orders, quote.makerAssetFillAmount, signatures) | ||||
|                 .getABIEncodedTransactionData(); | ||||
|         } else { | ||||
|             calldataHexString = this._exchangeContract | ||||
|                 .marketSellOrdersFillOrKill(orders, quote.takerAssetFillAmount, signatures) | ||||
|                 .getABIEncodedTransactionData(); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             calldataHexString, | ||||
|             ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount, | ||||
|             toAddress: this._exchangeContract.address, | ||||
|             allowanceTarget: this.contractAddresses.erc20Proxy, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public async executeSwapQuoteOrThrowAsync( | ||||
|         quote: SwapQuote, | ||||
|         opts: Partial<SwapQuoteExecutionOpts>, | ||||
|     ): Promise<string> { | ||||
|         assert.isValidSwapQuote('quote', quote); | ||||
|  | ||||
|         const { takerAddress, gasLimit, ethAmount } = opts; | ||||
|  | ||||
|         if (takerAddress !== undefined) { | ||||
|             assert.isETHAddressHex('takerAddress', takerAddress); | ||||
|         } | ||||
|         if (gasLimit !== undefined) { | ||||
|             assert.isNumber('gasLimit', gasLimit); | ||||
|         } | ||||
|         if (ethAmount !== undefined) { | ||||
|             assert.isBigNumber('ethAmount', ethAmount); | ||||
|         } | ||||
|         const { orders, gasPrice } = quote; | ||||
|         const signatures = orders.map(o => o.signature); | ||||
|  | ||||
|         const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); | ||||
|         const value = ethAmount || quote.worstCaseQuoteInfo.protocolFeeInWeiAmount; | ||||
|         let txHash: string; | ||||
|         if (quote.type === MarketOperation.Buy) { | ||||
|             const { makerAssetFillAmount } = quote; | ||||
|             txHash = await this._exchangeContract | ||||
|                 .marketBuyOrdersFillOrKill(orders, makerAssetFillAmount, signatures) | ||||
|                 .sendTransactionAsync({ | ||||
|                     from: finalTakerAddress, | ||||
|                     gas: gasLimit, | ||||
|                     gasPrice, | ||||
|                     value, | ||||
|                 }); | ||||
|         } else { | ||||
|             const { takerAssetFillAmount } = quote; | ||||
|             txHash = await this._exchangeContract | ||||
|                 .marketSellOrdersFillOrKill(orders, takerAssetFillAmount, signatures) | ||||
|                 .sendTransactionAsync({ | ||||
|                     from: finalTakerAddress, | ||||
|                     gas: gasLimit, | ||||
|                     gasPrice, | ||||
|                     value, | ||||
|                 }); | ||||
|         } | ||||
|         // TODO(dorothy-zbornak): Handle signature request denied | ||||
|         // (see contract-wrappers/decorators) | ||||
|         // and ExchangeRevertErrors.IncompleteFillError. | ||||
|         return txHash; | ||||
|     } | ||||
| } | ||||
| @@ -1,198 +0,0 @@ | ||||
| import { ContractAddresses } from '@0x/contract-addresses'; | ||||
| import { ForwarderContract } from '@0x/contract-wrappers'; | ||||
| import { assetDataUtils } from '@0x/order-utils'; | ||||
| import { providerUtils } from '@0x/utils'; | ||||
| import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { constants } from '../constants'; | ||||
| import { | ||||
|     CalldataInfo, | ||||
|     MarketOperation, | ||||
|     SwapQuote, | ||||
|     SwapQuoteConsumerBase, | ||||
|     SwapQuoteConsumerOpts, | ||||
|     SwapQuoteExecutionOpts, | ||||
|     SwapQuoteGetOutputOpts, | ||||
| } from '../types'; | ||||
| import { affiliateFeeUtils } from '../utils/affiliate_fee_utils'; | ||||
| import { assert } from '../utils/assert'; | ||||
| import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; | ||||
|  | ||||
| const { NULL_ADDRESS } = constants; | ||||
|  | ||||
| export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|     public readonly provider: ZeroExProvider; | ||||
|     public readonly chainId: number; | ||||
|     public buyQuoteSellAmountScalingFactor = 1.0001; // 100% + 1 bps | ||||
|  | ||||
|     private readonly _forwarder: ForwarderContract; | ||||
|  | ||||
|     constructor( | ||||
|         supportedProvider: SupportedProvider, | ||||
|         public readonly contractAddresses: ContractAddresses, | ||||
|         options: Partial<SwapQuoteConsumerOpts> = {}, | ||||
|     ) { | ||||
|         const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); | ||||
|         assert.isNumber('chainId', chainId); | ||||
|         const provider = providerUtils.standardizeOrThrow(supportedProvider); | ||||
|         this.provider = provider; | ||||
|         this.chainId = chainId; | ||||
|         this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given a SwapQuote, returns 'CalldataInfo' for a forwarder extension call. See type definition of CalldataInfo for more information. | ||||
|      * @param quote An object that conforms to SwapQuote. See type definition for more information. | ||||
|      * @param opts  Options for getting CalldataInfo. See type definition for more information. | ||||
|      */ | ||||
|     public async getCalldataOrThrowAsync( | ||||
|         quote: SwapQuote, | ||||
|         opts: Partial<SwapQuoteGetOutputOpts> = {}, | ||||
|     ): Promise<CalldataInfo> { | ||||
|         assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); | ||||
|         const { extensionContractOpts } = { ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, ...opts }; | ||||
|         assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts); | ||||
|         const { feeRecipient, feePercentage } = extensionContractOpts; | ||||
|         const { orders, worstCaseQuoteInfo } = quote; | ||||
|  | ||||
|         const normalizedFeeRecipientAddress = feeRecipient.toLowerCase(); | ||||
|         const signatures = _.map(orders, o => o.signature); | ||||
|         const ethAmountWithFees = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee( | ||||
|             { | ||||
|                 ...worstCaseQuoteInfo, | ||||
|                 // HACK(dorothy-zbornak): The forwarder contract has a rounding bug | ||||
|                 // that causes buys of low-decimal tokens to not complete. | ||||
|                 // Scaling the max sell amount by 1bps seems to be sufficient to | ||||
|                 // overcome this. | ||||
|                 ...(quote.type === MarketOperation.Buy | ||||
|                     ? { | ||||
|                           // tslint:disable-next-line: custom-no-magic-numbers | ||||
|                           totalTakerAssetAmount: worstCaseQuoteInfo.totalTakerAssetAmount | ||||
|                               .times(this.buyQuoteSellAmountScalingFactor) | ||||
|                               .integerValue(), | ||||
|                       } | ||||
|                     : {}), | ||||
|             }, | ||||
|             feePercentage, | ||||
|         ); | ||||
|         const feeAmount = affiliateFeeUtils.getFeeAmount(worstCaseQuoteInfo, feePercentage); | ||||
|  | ||||
|         let calldataHexString; | ||||
|         if (quote.type === MarketOperation.Buy) { | ||||
|             calldataHexString = this._forwarder | ||||
|                 .marketBuyOrdersWithEth( | ||||
|                     orders, | ||||
|                     quote.makerAssetFillAmount, | ||||
|                     signatures, | ||||
|                     [feeAmount], | ||||
|                     [normalizedFeeRecipientAddress], | ||||
|                 ) | ||||
|                 .getABIEncodedTransactionData(); | ||||
|         } else { | ||||
|             calldataHexString = this._forwarder | ||||
|                 .marketSellAmountWithEth( | ||||
|                     orders, | ||||
|                     quote.takerAssetFillAmount, | ||||
|                     signatures, | ||||
|                     [feeAmount], | ||||
|                     [normalizedFeeRecipientAddress], | ||||
|                 ) | ||||
|                 .getABIEncodedTransactionData(); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             calldataHexString, | ||||
|             toAddress: this._forwarder.address, | ||||
|             ethAmount: ethAmountWithFees, | ||||
|             allowanceTarget: NULL_ADDRESS, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given a SwapQuote and desired rate (in Eth), attempt to execute the swap. | ||||
|      * @param quote An object that conforms to SwapQuote. See type definition for more information. | ||||
|      * @param opts  Options for getting CalldataInfo. See type definition for more information. | ||||
|      */ | ||||
|     public async executeSwapQuoteOrThrowAsync( | ||||
|         quote: SwapQuote, | ||||
|         opts: Partial<SwapQuoteExecutionOpts>, | ||||
|     ): Promise<string> { | ||||
|         assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); | ||||
|  | ||||
|         const { ethAmount: providedEthAmount, takerAddress, gasLimit, extensionContractOpts } = { | ||||
|             ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, | ||||
|             ...opts, | ||||
|         }; | ||||
|  | ||||
|         assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts); | ||||
|  | ||||
|         const { feeRecipient, feePercentage } = extensionContractOpts; | ||||
|  | ||||
|         if (providedEthAmount !== undefined) { | ||||
|             assert.isBigNumber('ethAmount', providedEthAmount); | ||||
|         } | ||||
|         if (takerAddress !== undefined) { | ||||
|             assert.isETHAddressHex('takerAddress', takerAddress); | ||||
|         } | ||||
|         if (gasLimit !== undefined) { | ||||
|             assert.isNumber('gasLimit', gasLimit); | ||||
|         } | ||||
|         const { orders, gasPrice } = quote; // tslint:disable-line:no-unused-variable | ||||
|         const signatures = orders.map(o => o.signature); | ||||
|  | ||||
|         // get taker address | ||||
|         const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); | ||||
|         // if no ethAmount is provided, default to the worst totalTakerAssetAmount | ||||
|         const ethAmountWithFees = | ||||
|             providedEthAmount || | ||||
|             affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(quote.worstCaseQuoteInfo, feePercentage); | ||||
|         const feeAmount = affiliateFeeUtils.getFeeAmount( | ||||
|             { | ||||
|                 ...quote.worstCaseQuoteInfo, | ||||
|                 // HACK(dorothy-zbornak): The forwarder contract has a rounding bug | ||||
|                 // that causes buys of low-decimal tokens to not complete. | ||||
|                 // Scaling the max sell amount by 1bps seems to be sufficient to | ||||
|                 // overcome this. | ||||
|                 ...(quote.type === MarketOperation.Buy | ||||
|                     ? { | ||||
|                           // tslint:disable-next-line: custom-no-magic-numbers | ||||
|                           totalTakerAssetAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount | ||||
|                               .times(this.buyQuoteSellAmountScalingFactor) | ||||
|                               .integerValue(), | ||||
|                       } | ||||
|                     : {}), | ||||
|             }, | ||||
|             feePercentage, | ||||
|         ); | ||||
|         let txHash: string; | ||||
|         if (quote.type === MarketOperation.Buy) { | ||||
|             const { makerAssetFillAmount } = quote; | ||||
|             txHash = await this._forwarder | ||||
|                 .marketBuyOrdersWithEth(orders, makerAssetFillAmount, signatures, [feeAmount], [feeRecipient]) | ||||
|                 .sendTransactionAsync({ | ||||
|                     from: finalTakerAddress, | ||||
|                     gas: gasLimit, | ||||
|                     gasPrice, | ||||
|                     value: ethAmountWithFees, | ||||
|                 }); | ||||
|         } else { | ||||
|             txHash = await this._forwarder | ||||
|                 .marketSellAmountWithEth(orders, quote.takerAssetFillAmount, signatures, [feeAmount], [feeRecipient]) | ||||
|                 .sendTransactionAsync({ | ||||
|                     from: finalTakerAddress, | ||||
|                     gas: gasLimit, | ||||
|                     gasPrice, | ||||
|                     value: ethAmountWithFees, | ||||
|                 }); | ||||
|         } | ||||
|         // TODO(dorothy-zbornak): Handle signature request denied | ||||
|         // (see contract-wrappers/decorators) | ||||
|         // and ForwarderRevertErrors.CompleteBuyFailed. | ||||
|         return txHash; | ||||
|     } | ||||
|  | ||||
|     private _getEtherTokenAssetDataOrThrow(): string { | ||||
|         return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken); | ||||
|     } | ||||
| } | ||||
| @@ -18,15 +18,11 @@ import { assert } from '../utils/assert'; | ||||
| import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; | ||||
|  | ||||
| import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer'; | ||||
| import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer'; | ||||
| import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer'; | ||||
|  | ||||
| export class SwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|     public readonly provider: ZeroExProvider; | ||||
|     public readonly chainId: number; | ||||
|  | ||||
|     private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer; | ||||
|     private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer; | ||||
|     private readonly _contractAddresses: ContractAddresses; | ||||
|     private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer; | ||||
|  | ||||
| @@ -45,8 +41,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|         this.provider = provider; | ||||
|         this.chainId = chainId; | ||||
|         this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); | ||||
|         this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); | ||||
|         this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); | ||||
|         this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer( | ||||
|             supportedProvider, | ||||
|             this._contractAddresses, | ||||
| @@ -100,13 +94,12 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|     } | ||||
|  | ||||
|     private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> { | ||||
|         // ( akroeger)leaving this switch to use different contracts in the future | ||||
|         switch (opts.useExtensionContract) { | ||||
|             case ExtensionContractType.Forwarder: | ||||
|                 return this._forwarderConsumer; | ||||
|             case ExtensionContractType.ExchangeProxy: | ||||
|                 return this._exchangeProxyConsumer; | ||||
|             default: | ||||
|                 return this._exchangeConsumer; | ||||
|                 return this._exchangeProxyConsumer; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import { | ||||
|     TokenAdjacencyGraph, | ||||
| } from './utils/market_operation_utils/types'; | ||||
| import { QuoteReport } from './utils/quote_report_generator'; | ||||
| import { LogFunction } from './utils/quote_requestor'; | ||||
|  | ||||
| /** | ||||
|  * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). | ||||
| @@ -273,7 +272,7 @@ export interface RfqtMakerAssetOfferings { | ||||
|     [endpoint: string]: Array<[string, string]>; | ||||
| } | ||||
|  | ||||
| export { LogFunction } from './utils/quote_requestor'; | ||||
| export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; | ||||
|  | ||||
| export interface SwapQuoterRfqtOpts { | ||||
|     takerApiKeyWhitelist: string[]; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils'; | ||||
| import { SourceFilters } from './source_filters'; | ||||
| import { CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, GetMarketOrdersOpts } from './types'; | ||||
|  | ||||
| // tslint:disable: custom-no-magic-numbers | ||||
| // tslint:disable: custom-no-magic-numbers no-bitwise | ||||
|  | ||||
| /** | ||||
|  * Valid sources for market sell. | ||||
| @@ -22,6 +22,7 @@ export const SELL_SOURCE_FILTER = new SourceFilters([ | ||||
|     ERC20BridgeSource.Mooniswap, | ||||
|     ERC20BridgeSource.Swerve, | ||||
|     ERC20BridgeSource.SushiSwap, | ||||
|     ERC20BridgeSource.Shell, | ||||
|     ERC20BridgeSource.MultiHop, | ||||
| ]); | ||||
|  | ||||
| @@ -40,6 +41,7 @@ export const BUY_SOURCE_FILTER = new SourceFilters( | ||||
|         // ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes | ||||
|         ERC20BridgeSource.MStable, | ||||
|         ERC20BridgeSource.Mooniswap, | ||||
|         ERC20BridgeSource.Shell, | ||||
|         ERC20BridgeSource.Swerve, | ||||
|         ERC20BridgeSource.SushiSwap, | ||||
|         ERC20BridgeSource.MultiHop, | ||||
| @@ -58,8 +60,8 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | ||||
|     sampleDistributionBase: 1.05, | ||||
|     feeSchedule: {}, | ||||
|     gasSchedule: {}, | ||||
|     exchangeProxyOverhead: () => ZERO_AMOUNT, | ||||
|     allowFallback: true, | ||||
|     shouldBatchBridgeOrders: true, | ||||
|     shouldGenerateQuoteReport: false, | ||||
| }; | ||||
|  | ||||
| @@ -68,6 +70,11 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | ||||
|  */ | ||||
| export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2]; | ||||
|  | ||||
| export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign( | ||||
|     {}, | ||||
|     ...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })), | ||||
| ); | ||||
|  | ||||
| /** | ||||
|  * Mainnet Curve configuration | ||||
|  */ | ||||
|   | ||||
| @@ -3,15 +3,15 @@ import { BigNumber, hexUtils } from '@0x/utils'; | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; | ||||
| import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; | ||||
|  | ||||
| import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; | ||||
| import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types'; | ||||
| import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; | ||||
| import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types'; | ||||
|  | ||||
| // tslint:disable: prefer-for-of no-bitwise completed-docs | ||||
|  | ||||
| /** | ||||
|  * Create fill paths from orders and dex quotes. | ||||
|  * Create `Fill` objects from orders and dex quotes. | ||||
|  */ | ||||
| export function createFillPaths(opts: { | ||||
| export function createFills(opts: { | ||||
|     side: MarketOperation; | ||||
|     orders?: SignedOrderWithFillableAmounts[]; | ||||
|     dexQuotes?: DexSample[][]; | ||||
| @@ -28,30 +28,50 @@ export function createFillPaths(opts: { | ||||
|     const dexQuotes = opts.dexQuotes || []; | ||||
|     const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; | ||||
|     const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; | ||||
|     // Create native fill paths. | ||||
|     const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, ethToInputRate, feeSchedule); | ||||
|     // Create DEX fill paths. | ||||
|     const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule); | ||||
|     return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources); | ||||
|     // Create native fills. | ||||
|     const nativeFills = nativeOrdersToFills( | ||||
|         side, | ||||
|         orders, | ||||
|         opts.targetInput, | ||||
|         ethToOutputRate, | ||||
|         ethToInputRate, | ||||
|         feeSchedule, | ||||
|     ); | ||||
|     // Create DEX fills. | ||||
|     const dexFills = dexQuotes.map(singleSourceSamples => | ||||
|         dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule), | ||||
|     ); | ||||
|     return [...dexFills, nativeFills] | ||||
|         .map(p => clipFillsToInput(p, opts.targetInput)) | ||||
|         .filter(fills => hasLiquidity(fills) && !excludedSources.includes(fills[0].source)); | ||||
| } | ||||
|  | ||||
| function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fill[][] { | ||||
|     return paths.filter(path => { | ||||
|         if (path.length === 0) { | ||||
| function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] { | ||||
|     const clipped: Fill[] = []; | ||||
|     let input = ZERO_AMOUNT; | ||||
|     for (const fill of fills) { | ||||
|         if (input.gte(targetInput)) { | ||||
|             break; | ||||
|         } | ||||
|         input = input.plus(fill.input); | ||||
|         clipped.push(fill); | ||||
|     } | ||||
|     return clipped; | ||||
| } | ||||
|  | ||||
| function hasLiquidity(fills: Fill[]): boolean { | ||||
|     if (fills.length === 0) { | ||||
|         return false; | ||||
|     } | ||||
|         const [input, output] = getPathSize(path); | ||||
|         if (input.eq(0) || output.eq(0)) { | ||||
|             return false; | ||||
|         } | ||||
|         if (excludedSources.includes(path[0].source)) { | ||||
|     const totalInput = BigNumber.sum(...fills.map(fill => fill.input)); | ||||
|     const totalOutput = BigNumber.sum(...fills.map(fill => fill.output)); | ||||
|     if (totalInput.isZero() || totalOutput.isZero()) { | ||||
|         return false; | ||||
|     } | ||||
|     return true; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function nativeOrdersToPath( | ||||
| function nativeOrdersToFills( | ||||
|     side: MarketOperation, | ||||
|     orders: SignedOrderWithFillableAmounts[], | ||||
|     targetInput: BigNumber = POSITIVE_INF, | ||||
| @@ -61,7 +81,7 @@ function nativeOrdersToPath( | ||||
| ): Fill[] { | ||||
|     const sourcePathId = hexUtils.random(); | ||||
|     // Create a single path from all orders. | ||||
|     let path: Array<Fill & { adjustedRate: BigNumber }> = []; | ||||
|     let fills: Array<Fill & { adjustedRate: BigNumber }> = []; | ||||
|     for (const order of orders) { | ||||
|         const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order); | ||||
|         const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); | ||||
| @@ -87,13 +107,13 @@ function nativeOrdersToPath( | ||||
|         if (adjustedRate.lte(0)) { | ||||
|             continue; | ||||
|         } | ||||
|         path.push({ | ||||
|         fills.push({ | ||||
|             sourcePathId, | ||||
|             adjustedRate, | ||||
|             adjustedOutput, | ||||
|             input: clippedInput, | ||||
|             output: clippedOutput, | ||||
|             flags: 0, | ||||
|             flags: SOURCE_FLAGS[ERC20BridgeSource.Native], | ||||
|             index: 0, // TBD | ||||
|             parent: undefined, // TBD | ||||
|             source: ERC20BridgeSource.Native, | ||||
| @@ -101,44 +121,46 @@ function nativeOrdersToPath( | ||||
|         }); | ||||
|     } | ||||
|     // Sort by descending adjusted rate. | ||||
|     path = path.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate)); | ||||
|     fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate)); | ||||
|     // Re-index fills. | ||||
|     for (let i = 0; i < path.length; ++i) { | ||||
|         path[i].parent = i === 0 ? undefined : path[i - 1]; | ||||
|         path[i].index = i; | ||||
|     for (let i = 0; i < fills.length; ++i) { | ||||
|         fills[i].parent = i === 0 ? undefined : fills[i - 1]; | ||||
|         fills[i].index = i; | ||||
|     } | ||||
|     return path; | ||||
|     return fills; | ||||
| } | ||||
|  | ||||
| function dexQuotesToPaths( | ||||
| function dexSamplesToFills( | ||||
|     side: MarketOperation, | ||||
|     dexQuotes: DexSample[][], | ||||
|     samples: DexSample[], | ||||
|     ethToOutputRate: BigNumber, | ||||
|     ethToInputRate: BigNumber, | ||||
|     fees: FeeSchedule, | ||||
| ): Fill[][] { | ||||
|     const paths: Fill[][] = []; | ||||
|     for (let quote of dexQuotes) { | ||||
| ): Fill[] { | ||||
|     const sourcePathId = hexUtils.random(); | ||||
|         const path: Fill[] = []; | ||||
|     const fills: Fill[] = []; | ||||
|     // Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves | ||||
|     // We need not worry about Kyber fills going to UniswapReserve as the input amount | ||||
|     // we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input | ||||
|     // and we only fill [2,3] on Kyber (as 1 returns 0 output) | ||||
|         quote = quote.filter(q => !q.output.isZero()); | ||||
|         for (let i = 0; i < quote.length; i++) { | ||||
|             const sample = quote[i]; | ||||
|             const prevSample = i === 0 ? undefined : quote[i - 1]; | ||||
|     const nonzeroSamples = samples.filter(q => !q.output.isZero()); | ||||
|     for (let i = 0; i < nonzeroSamples.length; i++) { | ||||
|         const sample = nonzeroSamples[i]; | ||||
|         const prevSample = i === 0 ? undefined : nonzeroSamples[i - 1]; | ||||
|         const { source, fillData } = sample; | ||||
|         const input = sample.input.minus(prevSample ? prevSample.input : 0); | ||||
|         const output = sample.output.minus(prevSample ? prevSample.output : 0); | ||||
|         const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData); | ||||
|             const penalty = | ||||
|                 i === 0 // Only the first fill in a DEX path incurs a penalty. | ||||
|         let penalty = ZERO_AMOUNT; | ||||
|         if (i === 0) { | ||||
|             // Only the first fill in a DEX path incurs a penalty. | ||||
|             penalty = !ethToOutputRate.isZero() | ||||
|                 ? ethToOutputRate.times(fee) | ||||
|                     : ZERO_AMOUNT; | ||||
|                 : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); | ||||
|         } | ||||
|         const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||
|  | ||||
|             path.push({ | ||||
|         fills.push({ | ||||
|             sourcePathId, | ||||
|             input, | ||||
|             output, | ||||
| @@ -146,195 +168,9 @@ function dexQuotesToPaths( | ||||
|             source, | ||||
|             fillData, | ||||
|             index: i, | ||||
|                 parent: i !== 0 ? path[path.length - 1] : undefined, | ||||
|                 flags: sourceToFillFlags(source), | ||||
|             parent: i !== 0 ? fills[fills.length - 1] : undefined, | ||||
|             flags: SOURCE_FLAGS[source], | ||||
|         }); | ||||
|     } | ||||
|         paths.push(path); | ||||
|     } | ||||
|     return paths; | ||||
| } | ||||
|  | ||||
| export function getTwoHopAdjustedRate( | ||||
|     side: MarketOperation, | ||||
|     twoHopQuote: DexSample<MultiHopFillData>, | ||||
|     targetInput: BigNumber, | ||||
|     ethToOutputRate: BigNumber, | ||||
|     fees: FeeSchedule = {}, | ||||
| ): BigNumber { | ||||
|     const { output, input, fillData } = twoHopQuote; | ||||
|     if (input.isLessThan(targetInput) || output.isZero()) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData)); | ||||
|     const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||
|     return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); | ||||
| } | ||||
|  | ||||
| function sourceToFillFlags(source: ERC20BridgeSource): number { | ||||
|     switch (source) { | ||||
|         case ERC20BridgeSource.Uniswap: | ||||
|             return FillFlags.ConflictsWithMultiBridge; | ||||
|         case ERC20BridgeSource.LiquidityProvider: | ||||
|             return FillFlags.ConflictsWithMultiBridge; | ||||
|         case ERC20BridgeSource.MultiBridge: | ||||
|             return FillFlags.MultiBridge; | ||||
|         default: | ||||
|             return 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function getPathSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] { | ||||
|     let input = ZERO_AMOUNT; | ||||
|     let output = ZERO_AMOUNT; | ||||
|     for (const fill of path) { | ||||
|         if (input.plus(fill.input).gte(targetInput)) { | ||||
|             const di = targetInput.minus(input); | ||||
|             input = input.plus(di); | ||||
|             output = output.plus(fill.output.times(di.div(fill.input))); | ||||
|             break; | ||||
|         } else { | ||||
|             input = input.plus(fill.input); | ||||
|             output = output.plus(fill.output); | ||||
|         } | ||||
|     } | ||||
|     return [input.integerValue(), output.integerValue()]; | ||||
| } | ||||
|  | ||||
| export function getPathAdjustedSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] { | ||||
|     let input = ZERO_AMOUNT; | ||||
|     let output = ZERO_AMOUNT; | ||||
|     for (const fill of path) { | ||||
|         if (input.plus(fill.input).gte(targetInput)) { | ||||
|             const di = targetInput.minus(input); | ||||
|             if (di.gt(0)) { | ||||
|                 input = input.plus(di); | ||||
|                 // Penalty does not get interpolated. | ||||
|                 const penalty = fill.adjustedOutput.minus(fill.output); | ||||
|                 output = output.plus(fill.output.times(di.div(fill.input)).plus(penalty)); | ||||
|             } | ||||
|             break; | ||||
|         } else { | ||||
|             input = input.plus(fill.input); | ||||
|             output = output.plus(fill.adjustedOutput); | ||||
|         } | ||||
|     } | ||||
|     return [input.integerValue(), output.integerValue()]; | ||||
| } | ||||
|  | ||||
| export function isValidPath(path: Fill[], skipDuplicateCheck: boolean = false): boolean { | ||||
|     let flags = 0; | ||||
|     for (let i = 0; i < path.length; ++i) { | ||||
|         // Fill must immediately follow its parent. | ||||
|         if (path[i].parent) { | ||||
|             if (i === 0 || path[i - 1] !== path[i].parent) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         if (!skipDuplicateCheck) { | ||||
|             // Fill must not be duplicated. | ||||
|             for (let j = 0; j < i; ++j) { | ||||
|                 if (path[i] === path[j]) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         flags |= path[i].flags; | ||||
|     } | ||||
|     return arePathFlagsAllowed(flags); | ||||
| } | ||||
|  | ||||
| export function arePathFlagsAllowed(flags: number): boolean { | ||||
|     const multiBridgeConflict = FillFlags.MultiBridge | FillFlags.ConflictsWithMultiBridge; | ||||
|     return (flags & multiBridgeConflict) !== multiBridgeConflict; | ||||
| } | ||||
|  | ||||
| export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] { | ||||
|     const clipped: Fill[] = []; | ||||
|     let input = ZERO_AMOUNT; | ||||
|     for (const fill of path) { | ||||
|         if (input.gte(targetInput)) { | ||||
|             break; | ||||
|         } | ||||
|         input = input.plus(fill.input); | ||||
|         clipped.push(fill); | ||||
|     } | ||||
|     return clipped; | ||||
| } | ||||
|  | ||||
| export function collapsePath(path: Fill[]): CollapsedFill[] { | ||||
|     const collapsed: CollapsedFill[] = []; | ||||
|     for (const fill of path) { | ||||
|         const source = fill.source; | ||||
|         if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { | ||||
|             const prevFill = collapsed[collapsed.length - 1]; | ||||
|             // If the last fill is from the same source, merge them. | ||||
|             if (prevFill.sourcePathId === fill.sourcePathId) { | ||||
|                 prevFill.input = prevFill.input.plus(fill.input); | ||||
|                 prevFill.output = prevFill.output.plus(fill.output); | ||||
|                 prevFill.fillData = fill.fillData; | ||||
|                 prevFill.subFills.push(fill); | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|         collapsed.push({ | ||||
|             sourcePathId: fill.sourcePathId, | ||||
|             source: fill.source, | ||||
|             fillData: fill.fillData, | ||||
|             input: fill.input, | ||||
|             output: fill.output, | ||||
|             subFills: [fill], | ||||
|         }); | ||||
|     } | ||||
|     return collapsed; | ||||
| } | ||||
|  | ||||
| export function getPathAdjustedCompleteRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { | ||||
|     const [input, output] = getPathAdjustedSize(path, targetInput); | ||||
|     return getCompleteRate(side, input, output, targetInput); | ||||
| } | ||||
|  | ||||
| export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { | ||||
|     const [input, output] = getPathAdjustedSize(path, targetInput); | ||||
|     return getRate(side, input, output); | ||||
| } | ||||
|  | ||||
| export function getPathAdjustedSlippage( | ||||
|     side: MarketOperation, | ||||
|     path: Fill[], | ||||
|     inputAmount: BigNumber, | ||||
|     maxRate: BigNumber, | ||||
| ): number { | ||||
|     if (maxRate.eq(0)) { | ||||
|         return 0; | ||||
|     } | ||||
|     const totalRate = getPathAdjustedRate(side, path, inputAmount); | ||||
|     const rateChange = maxRate.minus(totalRate); | ||||
|     return rateChange.div(maxRate).toNumber(); | ||||
| } | ||||
|  | ||||
| export function getCompleteRate( | ||||
|     side: MarketOperation, | ||||
|     input: BigNumber, | ||||
|     output: BigNumber, | ||||
|     targetInput: BigNumber, | ||||
| ): BigNumber { | ||||
|     if (input.eq(0) || output.eq(0) || targetInput.eq(0)) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     // Penalize paths that fall short of the entire input amount by a factor of | ||||
|     // input / targetInput => (i / t) | ||||
|     if (side === MarketOperation.Sell) { | ||||
|         // (o / i) * (i / t) => (o / t) | ||||
|         return output.div(targetInput); | ||||
|     } | ||||
|     // (i / o) * (i / t) | ||||
|     return input.div(output).times(input.div(targetInput)); | ||||
| } | ||||
|  | ||||
| export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { | ||||
|     if (input.eq(0) || output.eq(0)) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     return side === MarketOperation.Sell ? output.div(input) : input.div(output); | ||||
|     return fills; | ||||
| } | ||||
|   | ||||
| @@ -7,19 +7,19 @@ import * as _ from 'lodash'; | ||||
| import { MarketOperation } from '../../types'; | ||||
| import { QuoteRequestor } from '../quote_requestor'; | ||||
|  | ||||
| import { generateQuoteReport } from './../quote_report_generator'; | ||||
| import { generateQuoteReport, QuoteReport } from './../quote_report_generator'; | ||||
| import { | ||||
|     BUY_SOURCE_FILTER, | ||||
|     DEFAULT_GET_MARKET_ORDERS_OPTS, | ||||
|     FEE_QUOTE_SOURCES, | ||||
|     ONE_ETHER, | ||||
|     SELL_SOURCE_FILTER, | ||||
|     SOURCE_FLAGS, | ||||
|     ZERO_AMOUNT, | ||||
| } from './constants'; | ||||
| import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; | ||||
| import { createFills } from './fills'; | ||||
| import { getBestTwoHopQuote } from './multihop_utils'; | ||||
| import { | ||||
|     createOrdersFromPath, | ||||
|     createOrdersFromTwoHopSample, | ||||
|     createSignedOrdersFromRfqtIndicativeQuotes, | ||||
|     createSignedOrdersWithFillableAmounts, | ||||
| @@ -30,8 +30,10 @@ import { DexOrderSampler, getSampleAmounts } from './sampler'; | ||||
| import { SourceFilters } from './source_filters'; | ||||
| import { | ||||
|     AggregationError, | ||||
|     CollapsedFill, | ||||
|     DexSample, | ||||
|     ERC20BridgeSource, | ||||
|     ExchangeProxyOverhead, | ||||
|     FeeSchedule, | ||||
|     GetMarketOrdersOpts, | ||||
|     MarketSideLiquidity, | ||||
| @@ -78,6 +80,25 @@ export class MarketOperationUtils { | ||||
|     private readonly _buySources: SourceFilters; | ||||
|     private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES); | ||||
|  | ||||
|     private static _computeQuoteReport( | ||||
|         nativeOrders: SignedOrder[], | ||||
|         quoteRequestor: QuoteRequestor | undefined, | ||||
|         marketSideLiquidity: MarketSideLiquidity, | ||||
|         optimizerResult: OptimizerResult, | ||||
|     ): QuoteReport { | ||||
|         const { side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; | ||||
|         const { liquidityDelivered } = optimizerResult; | ||||
|         return generateQuoteReport( | ||||
|             side, | ||||
|             _.flatten(dexQuotes), | ||||
|             twoHopQuotes, | ||||
|             nativeOrders, | ||||
|             orderFillableAmounts, | ||||
|             liquidityDelivered, | ||||
|             quoteRequestor, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     constructor( | ||||
|         private readonly _sampler: DexOrderSampler, | ||||
|         private readonly contractAddresses: ContractAddresses, | ||||
| @@ -342,16 +363,26 @@ export class MarketOperationUtils { | ||||
|     ): Promise<OptimizerResult> { | ||||
|         const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; | ||||
|         const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); | ||||
|         return this._generateOptimizedOrdersAsync(marketSideLiquidity, { | ||||
|         const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { | ||||
|             bridgeSlippage: _opts.bridgeSlippage, | ||||
|             maxFallbackSlippage: _opts.maxFallbackSlippage, | ||||
|             excludedSources: _opts.excludedSources, | ||||
|             feeSchedule: _opts.feeSchedule, | ||||
|             exchangeProxyOverhead: _opts.exchangeProxyOverhead, | ||||
|             allowFallback: _opts.allowFallback, | ||||
|             shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, | ||||
|             quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, | ||||
|             shouldGenerateQuoteReport: _opts.shouldGenerateQuoteReport, | ||||
|         }); | ||||
|  | ||||
|         // Compute Quote Report and return the results. | ||||
|         let quoteReport: QuoteReport | undefined; | ||||
|         if (_opts.shouldGenerateQuoteReport) { | ||||
|             quoteReport = MarketOperationUtils._computeQuoteReport( | ||||
|                 nativeOrders, | ||||
|                 _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, | ||||
|                 marketSideLiquidity, | ||||
|                 optimizerResult, | ||||
|             ); | ||||
|         } | ||||
|         return { ...optimizerResult, quoteReport }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -369,16 +400,24 @@ export class MarketOperationUtils { | ||||
|     ): Promise<OptimizerResult> { | ||||
|         const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; | ||||
|         const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); | ||||
|         return this._generateOptimizedOrdersAsync(marketSideLiquidity, { | ||||
|         const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { | ||||
|             bridgeSlippage: _opts.bridgeSlippage, | ||||
|             maxFallbackSlippage: _opts.maxFallbackSlippage, | ||||
|             excludedSources: _opts.excludedSources, | ||||
|             feeSchedule: _opts.feeSchedule, | ||||
|             exchangeProxyOverhead: _opts.exchangeProxyOverhead, | ||||
|             allowFallback: _opts.allowFallback, | ||||
|             shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, | ||||
|             quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, | ||||
|             shouldGenerateQuoteReport: _opts.shouldGenerateQuoteReport, | ||||
|         }); | ||||
|         let quoteReport: QuoteReport | undefined; | ||||
|         if (_opts.shouldGenerateQuoteReport) { | ||||
|             quoteReport = MarketOperationUtils._computeQuoteReport( | ||||
|                 nativeOrders, | ||||
|                 _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, | ||||
|                 marketSideLiquidity, | ||||
|                 optimizerResult, | ||||
|             ); | ||||
|         } | ||||
|         return { ...optimizerResult, quoteReport }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -467,8 +506,6 @@ export class MarketOperationUtils { | ||||
|                             excludedSources: _opts.excludedSources, | ||||
|                             feeSchedule: _opts.feeSchedule, | ||||
|                             allowFallback: _opts.allowFallback, | ||||
|                             shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, | ||||
|                             shouldGenerateQuoteReport: false, | ||||
|                         }, | ||||
|                     ); | ||||
|                     return optimizedOrders; | ||||
| @@ -489,10 +526,9 @@ export class MarketOperationUtils { | ||||
|             maxFallbackSlippage?: number; | ||||
|             excludedSources?: ERC20BridgeSource[]; | ||||
|             feeSchedule?: FeeSchedule; | ||||
|             exchangeProxyOverhead?: ExchangeProxyOverhead; | ||||
|             allowFallback?: boolean; | ||||
|             shouldBatchBridgeOrders?: boolean; | ||||
|             quoteRequestor?: QuoteRequestor; | ||||
|             shouldGenerateQuoteReport?: boolean; | ||||
|         }, | ||||
|     ): Promise<OptimizerResult> { | ||||
|         const { | ||||
| @@ -506,7 +542,6 @@ export class MarketOperationUtils { | ||||
|             dexQuotes, | ||||
|             ethToOutputRate, | ||||
|             ethToInputRate, | ||||
|             twoHopQuotes, | ||||
|         } = marketSideLiquidity; | ||||
|         const maxFallbackSlippage = opts.maxFallbackSlippage || 0; | ||||
|  | ||||
| @@ -517,11 +552,10 @@ export class MarketOperationUtils { | ||||
|             orderDomain: this._orderDomain, | ||||
|             contractAddresses: this.contractAddresses, | ||||
|             bridgeSlippage: opts.bridgeSlippage || 0, | ||||
|             shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, | ||||
|         }; | ||||
|  | ||||
|         // Convert native orders and dex quotes into fill paths. | ||||
|         const paths = createFillPaths({ | ||||
|         // Convert native orders and dex quotes into `Fill` objects. | ||||
|         const fills = createFills({ | ||||
|             side, | ||||
|             // Augment native orders with their fillable amounts. | ||||
|             orders: [ | ||||
| @@ -537,72 +571,53 @@ export class MarketOperationUtils { | ||||
|         }); | ||||
|  | ||||
|         // Find the optimal path. | ||||
|         let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || []; | ||||
|         if (optimalPath.length === 0) { | ||||
|         const optimizerOpts = { | ||||
|             ethToOutputRate, | ||||
|             ethToInputRate, | ||||
|             exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), | ||||
|         }; | ||||
|         const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); | ||||
|         if (optimalPath === undefined) { | ||||
|             throw new Error(AggregationError.NoOptimalPath); | ||||
|         } | ||||
|         const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount); | ||||
|         const optimalPathRate = optimalPath.adjustedRate(); | ||||
|  | ||||
|         const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( | ||||
|             marketSideLiquidity, | ||||
|             opts.feeSchedule, | ||||
|             opts.exchangeProxyOverhead, | ||||
|         ); | ||||
|         if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { | ||||
|             const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); | ||||
|             const twoHopQuoteReport = opts.shouldGenerateQuoteReport | ||||
|                 ? generateQuoteReport( | ||||
|                       side, | ||||
|                       _.flatten(dexQuotes), | ||||
|                       twoHopQuotes, | ||||
|                       nativeOrders, | ||||
|                       orderFillableAmounts, | ||||
|                       bestTwoHopQuote, | ||||
|                       opts.quoteRequestor, | ||||
|                   ) | ||||
|                 : undefined; | ||||
|             return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true }; | ||||
|             return { | ||||
|                 optimizedOrders: twoHopOrders, | ||||
|                 liquidityDelivered: bestTwoHopQuote, | ||||
|                 sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // Generate a fallback path if native orders are in the optimal path. | ||||
|         const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native); | ||||
|         if (opts.allowFallback && nativeSubPath.length !== 0) { | ||||
|         const nativeFills = optimalPath.fills.filter(f => f.source === ERC20BridgeSource.Native); | ||||
|         if (opts.allowFallback && nativeFills.length !== 0) { | ||||
|             // We create a fallback path that is exclusive of Native liquidity | ||||
|             // This is the optimal on-chain path for the entire input amount | ||||
|             const nonNativePaths = paths.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native); | ||||
|             const nonNativeOptimalPath = | ||||
|                 (await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || []; | ||||
|             const nonNativeFills = fills.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native); | ||||
|             const nonNativeOptimalPath = await findOptimalPathAsync(side, nonNativeFills, inputAmount, opts.runLimit); | ||||
|             // Calculate the slippage of on-chain sources compared to the most optimal path | ||||
|             const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate); | ||||
|             if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) { | ||||
|                 // If the last fill is Native and penultimate is not, then the intention was to partial fill | ||||
|                 // In this case we drop it entirely as we can't handle a failure at the end and we don't | ||||
|                 // want to fully fill when it gets prepended to the front below | ||||
|                 const [last, penultimateIfExists] = optimalPath.slice().reverse(); | ||||
|                 const lastNativeFillIfExists = | ||||
|                     last.source === ERC20BridgeSource.Native && | ||||
|                     penultimateIfExists && | ||||
|                     penultimateIfExists.source !== ERC20BridgeSource.Native | ||||
|                         ? last | ||||
|                         : undefined; | ||||
|                 // By prepending native paths to the front they cannot split on-chain sources and incur | ||||
|                 // an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber] | ||||
|                 // In the previous step we dropped any hanging Native partial fills, as to not fully fill | ||||
|                 optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; | ||||
|             if ( | ||||
|                 nonNativeOptimalPath !== undefined && | ||||
|                 (nativeFills.length === optimalPath.fills.length || | ||||
|                     nonNativeOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage) | ||||
|             ) { | ||||
|                 optimalPath.addFallback(nonNativeOptimalPath); | ||||
|             } | ||||
|         } | ||||
|         const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts); | ||||
|         const quoteReport = opts.shouldGenerateQuoteReport | ||||
|             ? generateQuoteReport( | ||||
|                   side, | ||||
|                   _.flatten(dexQuotes), | ||||
|                   twoHopQuotes, | ||||
|                   nativeOrders, | ||||
|                   orderFillableAmounts, | ||||
|                   _.flatten(optimizedOrders.map(order => order.fills)), | ||||
|                   opts.quoteRequestor, | ||||
|               ) | ||||
|             : undefined; | ||||
|         return { optimizedOrders, quoteReport, isTwoHop: false }; | ||||
|         const collapsedPath = optimalPath.collapse(orderOpts); | ||||
|         return { | ||||
|             optimizedOrders: collapsedPath.orders, | ||||
|             liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[], | ||||
|             sourceFlags: collapsedPath.sourceFlags, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,15 @@ import { BigNumber } from '@0x/utils'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { ZERO_AMOUNT } from './constants'; | ||||
| import { getTwoHopAdjustedRate } from './fills'; | ||||
| import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types'; | ||||
| import { getTwoHopAdjustedRate } from './rate_utils'; | ||||
| import { | ||||
|     DexSample, | ||||
|     ExchangeProxyOverhead, | ||||
|     FeeSchedule, | ||||
|     MarketSideLiquidity, | ||||
|     MultiHopFillData, | ||||
|     TokenAdjacencyGraph, | ||||
| } from './types'; | ||||
|  | ||||
| /** | ||||
|  * Given a token pair, returns the intermediate tokens to consider for two-hop routes. | ||||
| @@ -36,18 +43,28 @@ export function getIntermediateTokens( | ||||
| export function getBestTwoHopQuote( | ||||
|     marketSideLiquidity: MarketSideLiquidity, | ||||
|     feeSchedule?: FeeSchedule, | ||||
|     exchangeProxyOverhead?: ExchangeProxyOverhead, | ||||
| ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { | ||||
|     const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity; | ||||
|     if (twoHopQuotes.length === 0) { | ||||
|         return { adjustedRate: ZERO_AMOUNT, quote: undefined }; | ||||
|     } | ||||
|     const best = twoHopQuotes | ||||
|         .map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule)) | ||||
|         .map(quote => | ||||
|             getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead), | ||||
|         ) | ||||
|         .reduce( | ||||
|             (prev, curr, i) => | ||||
|                 curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev, | ||||
|             { | ||||
|                 adjustedRate: getTwoHopAdjustedRate(side, twoHopQuotes[0], inputAmount, ethToOutputRate, feeSchedule), | ||||
|                 adjustedRate: getTwoHopAdjustedRate( | ||||
|                     side, | ||||
|                     twoHopQuotes[0], | ||||
|                     inputAmount, | ||||
|                     ethToOutputRate, | ||||
|                     feeSchedule, | ||||
|                     exchangeProxyOverhead, | ||||
|                 ), | ||||
|                 quote: twoHopQuotes[0], | ||||
|             }, | ||||
|         ); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ContractAddresses } from '@0x/contract-addresses'; | ||||
| import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; | ||||
| import { RFQTIndicativeQuote } from '@0x/quote-server'; | ||||
| import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; | ||||
| import { SignedOrder } from '@0x/types'; | ||||
| import { AbiEncoder, BigNumber } from '@0x/utils'; | ||||
|  | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; | ||||
| @@ -16,7 +16,6 @@ import { | ||||
|     WALLET_SIGNATURE, | ||||
|     ZERO_AMOUNT, | ||||
| } from './constants'; | ||||
| import { collapsePath } from './fills'; | ||||
| import { getMultiBridgeIntermediateToken } from './multibridge_utils'; | ||||
| import { | ||||
|     AggregationError, | ||||
| @@ -26,7 +25,6 @@ import { | ||||
|     CurveFillData, | ||||
|     DexSample, | ||||
|     ERC20BridgeSource, | ||||
|     Fill, | ||||
|     KyberFillData, | ||||
|     LiquidityProviderFillData, | ||||
|     MooniswapFillData, | ||||
| @@ -42,30 +40,6 @@ import { | ||||
|  | ||||
| // tslint:disable completed-docs no-unnecessary-type-assertion | ||||
|  | ||||
| interface DexForwaderBridgeData { | ||||
|     inputToken: string; | ||||
|     calls: Array<{ | ||||
|         target: string; | ||||
|         inputTokenAmount: BigNumber; | ||||
|         outputTokenAmount: BigNumber; | ||||
|         bridgeData: string; | ||||
|     }>; | ||||
| } | ||||
|  | ||||
| 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' }, | ||||
|         ], | ||||
|     }, | ||||
| ]); | ||||
|  | ||||
| export function createDummyOrderForSampler( | ||||
|     makerAssetData: string, | ||||
|     takerAssetData: string, | ||||
| @@ -152,38 +126,6 @@ export interface CreateOrderFromPathOpts { | ||||
|     orderDomain: OrderDomain; | ||||
|     contractAddresses: ContractAddresses; | ||||
|     bridgeSlippage: number; | ||||
|     shouldBatchBridgeOrders: boolean; | ||||
| } | ||||
|  | ||||
| // Convert sell fills into orders. | ||||
| export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { | ||||
|     const [makerToken, takerToken] = getMakerTakerTokens(opts); | ||||
|     const collapsedPath = collapsePath(path); | ||||
|     const orders: OptimizedMarketOrder[] = []; | ||||
|     for (let i = 0; i < collapsedPath.length; ) { | ||||
|         if (collapsedPath[i].source === ERC20BridgeSource.Native) { | ||||
|             orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill)); | ||||
|             ++i; | ||||
|             continue; | ||||
|         } | ||||
|         // If there are contiguous bridge orders, we can batch them together. | ||||
|         const contiguousBridgeFills = [collapsedPath[i]]; | ||||
|         for (let j = i + 1; j < collapsedPath.length; ++j) { | ||||
|             if (collapsedPath[j].source === ERC20BridgeSource.Native) { | ||||
|                 break; | ||||
|             } | ||||
|             contiguousBridgeFills.push(collapsedPath[j]); | ||||
|         } | ||||
|         // Always use DexForwarderBridge unless configured not to | ||||
|         if (!opts.shouldBatchBridgeOrders) { | ||||
|             orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); | ||||
|             i += 1; | ||||
|         } else { | ||||
|             orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); | ||||
|             i += contiguousBridgeFills.length; | ||||
|         } | ||||
|     } | ||||
|     return orders; | ||||
| } | ||||
|  | ||||
| export function createOrdersFromTwoHopSample( | ||||
| @@ -242,13 +184,15 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath | ||||
|             return opts.contractAddresses.mStableBridge; | ||||
|         case ERC20BridgeSource.Mooniswap: | ||||
|             return opts.contractAddresses.mooniswapBridge; | ||||
|         case ERC20BridgeSource.Shell: | ||||
|             return opts.contractAddresses.shellBridge; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|     throw new Error(AggregationError.NoBridgeForSource); | ||||
| } | ||||
|  | ||||
| function createBridgeOrder( | ||||
| export function createBridgeOrder( | ||||
|     fill: CollapsedFill, | ||||
|     makerToken: string, | ||||
|     takerToken: string, | ||||
| @@ -362,48 +306,7 @@ function createBridgeOrder( | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { | ||||
|     const [makerToken, takerToken] = getMakerTakerTokens(opts); | ||||
|     let totalMakerAssetAmount = ZERO_AMOUNT; | ||||
|     let totalTakerAssetAmount = ZERO_AMOUNT; | ||||
|     const batchedBridgeData: DexForwaderBridgeData = { | ||||
|         inputToken: takerToken, | ||||
|         calls: [], | ||||
|     }; | ||||
|     for (const fill of fills) { | ||||
|         const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts); | ||||
|         totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); | ||||
|         totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); | ||||
|         const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( | ||||
|             bridgeOrder.makerAssetData, | ||||
|         ) as ERC20BridgeAssetData; | ||||
|         batchedBridgeData.calls.push({ | ||||
|             target: bridgeAddress, | ||||
|             bridgeData: orderBridgeData, | ||||
|             inputTokenAmount: bridgeOrder.takerAssetAmount, | ||||
|             outputTokenAmount: bridgeOrder.makerAssetAmount, | ||||
|         }); | ||||
|     } | ||||
|     const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge; | ||||
|     const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData( | ||||
|         makerToken, | ||||
|         batchedBridgeAddress, | ||||
|         dexForwarderBridgeDataEncoder.encode(batchedBridgeData), | ||||
|     ); | ||||
|     return { | ||||
|         fills, | ||||
|         makerAssetData: batchedMakerAssetData, | ||||
|         takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), | ||||
|         makerAddress: batchedBridgeAddress, | ||||
|         makerAssetAmount: totalMakerAssetAmount, | ||||
|         takerAssetAmount: totalTakerAssetAmount, | ||||
|         fillableMakerAssetAmount: totalMakerAssetAmount, | ||||
|         fillableTakerAssetAmount: totalTakerAssetAmount, | ||||
|         ...createCommonBridgeOrderFields(opts.orderDomain), | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { | ||||
| export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { | ||||
|     const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; | ||||
|     const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; | ||||
|     return [makerToken, takerToken]; | ||||
| @@ -525,7 +428,7 @@ function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOr | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { | ||||
| export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { | ||||
|     return { | ||||
|         fills: [fill], | ||||
|         ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion | ||||
|   | ||||
							
								
								
									
										276
									
								
								packages/asset-swapper/src/utils/market_operation_utils/path.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								packages/asset-swapper/src/utils/market_operation_utils/path.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | ||||
| import { BigNumber } from '@0x/utils'; | ||||
|  | ||||
| import { MarketOperation } from '../../types'; | ||||
|  | ||||
| import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; | ||||
| import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; | ||||
| import { getCompleteRate, getRate } from './rate_utils'; | ||||
| import { | ||||
|     CollapsedFill, | ||||
|     ERC20BridgeSource, | ||||
|     ExchangeProxyOverhead, | ||||
|     Fill, | ||||
|     NativeCollapsedFill, | ||||
|     OptimizedMarketOrder, | ||||
| } from './types'; | ||||
|  | ||||
| // tslint:disable: prefer-for-of no-bitwise completed-docs | ||||
|  | ||||
| export interface PathSize { | ||||
|     input: BigNumber; | ||||
|     output: BigNumber; | ||||
| } | ||||
|  | ||||
| export interface PathPenaltyOpts { | ||||
|     ethToOutputRate: BigNumber; | ||||
|     ethToInputRate: BigNumber; | ||||
|     exchangeProxyOverhead: ExchangeProxyOverhead; | ||||
| } | ||||
|  | ||||
| export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { | ||||
|     ethToOutputRate: ZERO_AMOUNT, | ||||
|     ethToInputRate: ZERO_AMOUNT, | ||||
|     exchangeProxyOverhead: () => ZERO_AMOUNT, | ||||
| }; | ||||
|  | ||||
| export class Path { | ||||
|     public collapsedFills?: ReadonlyArray<CollapsedFill>; | ||||
|     public orders?: OptimizedMarketOrder[]; | ||||
|     public sourceFlags: number = 0; | ||||
|     protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; | ||||
|     protected _adjustedSize: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; | ||||
|  | ||||
|     public static create( | ||||
|         side: MarketOperation, | ||||
|         fills: ReadonlyArray<Fill>, | ||||
|         targetInput: BigNumber = POSITIVE_INF, | ||||
|         pathPenaltyOpts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS, | ||||
|     ): Path { | ||||
|         const path = new Path(side, fills, targetInput, pathPenaltyOpts); | ||||
|         fills.forEach(fill => { | ||||
|             path.sourceFlags |= fill.flags; | ||||
|             path._addFillSize(fill); | ||||
|         }); | ||||
|         return path; | ||||
|     } | ||||
|  | ||||
|     public static clone(base: Path): Path { | ||||
|         const clonedPath = new Path(base.side, base.fills.slice(), base.targetInput, base.pathPenaltyOpts); | ||||
|         clonedPath.sourceFlags = base.sourceFlags; | ||||
|         clonedPath._size = { ...base._size }; | ||||
|         clonedPath._adjustedSize = { ...base._adjustedSize }; | ||||
|         clonedPath.collapsedFills = base.collapsedFills === undefined ? undefined : base.collapsedFills.slice(); | ||||
|         clonedPath.orders = base.orders === undefined ? undefined : base.orders.slice(); | ||||
|         return clonedPath; | ||||
|     } | ||||
|  | ||||
|     protected constructor( | ||||
|         protected readonly side: MarketOperation, | ||||
|         public fills: ReadonlyArray<Fill>, | ||||
|         protected readonly targetInput: BigNumber, | ||||
|         public readonly pathPenaltyOpts: PathPenaltyOpts, | ||||
|     ) {} | ||||
|  | ||||
|     public append(fill: Fill): this { | ||||
|         (this.fills as Fill[]).push(fill); | ||||
|         this.sourceFlags |= fill.flags; | ||||
|         this._addFillSize(fill); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public addFallback(fallback: Path): this { | ||||
|         // If the last fill is Native and penultimate is not, then the intention was to partial fill | ||||
|         // In this case we drop it entirely as we can't handle a failure at the end and we don't | ||||
|         // want to fully fill when it gets prepended to the front below | ||||
|         const [last, penultimateIfExists] = this.fills.slice().reverse(); | ||||
|         const lastNativeFillIfExists = | ||||
|             last.source === ERC20BridgeSource.Native && | ||||
|             penultimateIfExists && | ||||
|             penultimateIfExists.source !== ERC20BridgeSource.Native | ||||
|                 ? last | ||||
|                 : undefined; | ||||
|         // By prepending native paths to the front they cannot split on-chain sources and incur | ||||
|         // an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber] | ||||
|         // In the previous step we dropped any hanging Native partial fills, as to not fully fill | ||||
|         const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native); | ||||
|         this.fills = [...nativeFills.filter(f => f !== lastNativeFillIfExists), ...fallback.fills]; | ||||
|         // Recompute the source flags | ||||
|         this.sourceFlags = this.fills.reduce((flags, fill) => flags | fill.flags, 0); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public collapse(opts: CreateOrderFromPathOpts): CollapsedPath { | ||||
|         const [makerToken, takerToken] = getMakerTakerTokens(opts); | ||||
|         const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills; | ||||
|         this.orders = []; | ||||
|         for (let i = 0; i < collapsedFills.length; ) { | ||||
|             if (collapsedFills[i].source === ERC20BridgeSource.Native) { | ||||
|                 this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill)); | ||||
|                 ++i; | ||||
|                 continue; | ||||
|             } | ||||
|             // If there are contiguous bridge orders, we can batch them together. | ||||
|             const contiguousBridgeFills = [collapsedFills[i]]; | ||||
|             for (let j = i + 1; j < collapsedFills.length; ++j) { | ||||
|                 if (collapsedFills[j].source === ERC20BridgeSource.Native) { | ||||
|                     break; | ||||
|                 } | ||||
|                 contiguousBridgeFills.push(collapsedFills[j]); | ||||
|             } | ||||
|  | ||||
|             this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); | ||||
|             i += 1; | ||||
|         } | ||||
|         return this as CollapsedPath; | ||||
|     } | ||||
|  | ||||
|     public size(): PathSize { | ||||
|         return this._size; | ||||
|     } | ||||
|  | ||||
|     public adjustedSize(): PathSize { | ||||
|         const { input, output } = this._adjustedSize; | ||||
|         const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts; | ||||
|         const gasOverhead = exchangeProxyOverhead(this.sourceFlags); | ||||
|         const pathPenalty = !ethToOutputRate.isZero() | ||||
|             ? ethToOutputRate.times(gasOverhead) | ||||
|             : ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input)); | ||||
|         return { | ||||
|             input, | ||||
|             output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public adjustedCompleteRate(): BigNumber { | ||||
|         const { input, output } = this.adjustedSize(); | ||||
|         return getCompleteRate(this.side, input, output, this.targetInput); | ||||
|     } | ||||
|  | ||||
|     public adjustedRate(): BigNumber { | ||||
|         const { input, output } = this.adjustedSize(); | ||||
|         return getRate(this.side, input, output); | ||||
|     } | ||||
|  | ||||
|     public adjustedSlippage(maxRate: BigNumber): number { | ||||
|         if (maxRate.eq(0)) { | ||||
|             return 0; | ||||
|         } | ||||
|         const totalRate = this.adjustedRate(); | ||||
|         const rateChange = maxRate.minus(totalRate); | ||||
|         return rateChange.div(maxRate).toNumber(); | ||||
|     } | ||||
|  | ||||
|     public isBetterThan(other: Path): boolean { | ||||
|         if (!this.targetInput.isEqualTo(other.targetInput)) { | ||||
|             throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`); | ||||
|         } | ||||
|         const { targetInput } = this; | ||||
|         const { input } = this._size; | ||||
|         const { input: otherInput } = other._size; | ||||
|         if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) { | ||||
|             return input.isGreaterThan(otherInput); | ||||
|         } else { | ||||
|             return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate()); | ||||
|         } | ||||
|         // if (otherInput.isLessThan(targetInput)) { | ||||
|         //     return input.isGreaterThan(otherInput); | ||||
|         // } else if (input.isGreaterThanOrEqualTo(targetInput)) { | ||||
|         //     return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate()); | ||||
|         // } | ||||
|         // return false; | ||||
|     } | ||||
|  | ||||
|     public isComplete(): boolean { | ||||
|         const { input } = this._size; | ||||
|         return input.gte(this.targetInput); | ||||
|     } | ||||
|  | ||||
|     public isValid(skipDuplicateCheck: boolean = false): boolean { | ||||
|         for (let i = 0; i < this.fills.length; ++i) { | ||||
|             // Fill must immediately follow its parent. | ||||
|             if (this.fills[i].parent) { | ||||
|                 if (i === 0 || this.fills[i - 1] !== this.fills[i].parent) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|             if (!skipDuplicateCheck) { | ||||
|                 // Fill must not be duplicated. | ||||
|                 for (let j = 0; j < i; ++j) { | ||||
|                     if (this.fills[i] === this.fills[j]) { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return doSourcesConflict(this.sourceFlags); | ||||
|     } | ||||
|  | ||||
|     public isValidNextFill(fill: Fill): boolean { | ||||
|         if (this.fills.length === 0) { | ||||
|             return !fill.parent; | ||||
|         } | ||||
|         if (this.fills[this.fills.length - 1] === fill.parent) { | ||||
|             return true; | ||||
|         } | ||||
|         if (fill.parent) { | ||||
|             return false; | ||||
|         } | ||||
|         return doSourcesConflict(this.sourceFlags | fill.flags); | ||||
|     } | ||||
|  | ||||
|     private _collapseFills(): ReadonlyArray<CollapsedFill> { | ||||
|         this.collapsedFills = []; | ||||
|         for (const fill of this.fills) { | ||||
|             const source = fill.source; | ||||
|             if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) { | ||||
|                 const prevFill = this.collapsedFills[this.collapsedFills.length - 1]; | ||||
|                 // If the last fill is from the same source, merge them. | ||||
|                 if (prevFill.sourcePathId === fill.sourcePathId) { | ||||
|                     prevFill.input = prevFill.input.plus(fill.input); | ||||
|                     prevFill.output = prevFill.output.plus(fill.output); | ||||
|                     prevFill.fillData = fill.fillData; | ||||
|                     prevFill.subFills.push(fill); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             (this.collapsedFills as CollapsedFill[]).push({ | ||||
|                 sourcePathId: fill.sourcePathId, | ||||
|                 source: fill.source, | ||||
|                 fillData: fill.fillData, | ||||
|                 input: fill.input, | ||||
|                 output: fill.output, | ||||
|                 subFills: [fill], | ||||
|             }); | ||||
|         } | ||||
|         return this.collapsedFills; | ||||
|     } | ||||
|  | ||||
|     private _addFillSize(fill: Fill): void { | ||||
|         if (this._size.input.plus(fill.input).isGreaterThan(this.targetInput)) { | ||||
|             const remainingInput = this.targetInput.minus(this._size.input); | ||||
|             const scaledFillOutput = fill.output.times(remainingInput.div(fill.input)); | ||||
|             this._size.input = this.targetInput; | ||||
|             this._size.output = this._size.output.plus(scaledFillOutput); | ||||
|             // Penalty does not get interpolated. | ||||
|             const penalty = fill.adjustedOutput.minus(fill.output); | ||||
|             this._adjustedSize.input = this.targetInput; | ||||
|             this._adjustedSize.output = this._adjustedSize.output.plus(scaledFillOutput).plus(penalty); | ||||
|         } else { | ||||
|             this._size.input = this._size.input.plus(fill.input); | ||||
|             this._size.output = this._size.output.plus(fill.output); | ||||
|             this._adjustedSize.input = this._adjustedSize.input.plus(fill.input); | ||||
|             this._adjustedSize.output = this._adjustedSize.output.plus(fill.adjustedOutput); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export interface CollapsedPath extends Path { | ||||
|     readonly collapsedFills: ReadonlyArray<CollapsedFill>; | ||||
|     readonly orders: OptimizedMarketOrder[]; | ||||
| } | ||||
|  | ||||
| const MULTIBRIDGE_SOURCES = SOURCE_FLAGS.LiquidityProvider | SOURCE_FLAGS.Uniswap; | ||||
| export function doSourcesConflict(flags: number): boolean { | ||||
|     const multiBridgeConflict = flags & SOURCE_FLAGS.MultiBridge && flags & MULTIBRIDGE_SOURCES; | ||||
|     return !multiBridgeConflict; | ||||
| } | ||||
| @@ -1,17 +1,9 @@ | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { MarketOperation } from '../../types'; | ||||
|  | ||||
| import { ZERO_AMOUNT } from './constants'; | ||||
| import { | ||||
|     arePathFlagsAllowed, | ||||
|     getCompleteRate, | ||||
|     getPathAdjustedCompleteRate, | ||||
|     getPathAdjustedRate, | ||||
|     getPathAdjustedSize, | ||||
|     getPathSize, | ||||
|     isValidPath, | ||||
| } from './fills'; | ||||
| import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; | ||||
| import { Fill } from './types'; | ||||
|  | ||||
| // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise | ||||
| @@ -19,134 +11,93 @@ import { Fill } from './types'; | ||||
| const RUN_LIMIT_DECAY_FACTOR = 0.5; | ||||
|  | ||||
| /** | ||||
|  * Find the optimal mixture of paths that maximizes (for sells) or minimizes | ||||
|  * Find the optimal mixture of fills that maximizes (for sells) or minimizes | ||||
|  * (for buys) output, while meeting the input requirement. | ||||
|  */ | ||||
| export async function findOptimalPathAsync( | ||||
|     side: MarketOperation, | ||||
|     paths: Fill[][], | ||||
|     fills: Fill[][], | ||||
|     targetInput: BigNumber, | ||||
|     runLimit: number = 2 ** 8, | ||||
| ): Promise<Fill[] | undefined> { | ||||
|     // Sort paths by descending adjusted completed rate. | ||||
|     const sortedPaths = paths | ||||
|         .slice(0) | ||||
|         .sort((a, b) => | ||||
|             getPathAdjustedCompleteRate(side, b, targetInput).comparedTo( | ||||
|                 getPathAdjustedCompleteRate(side, a, targetInput), | ||||
|             ), | ||||
|         ); | ||||
|     let optimalPath = sortedPaths[0] || []; | ||||
|     opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS, | ||||
| ): Promise<Path | undefined> { | ||||
|     const rates = rateBySourcePathId(side, fills, targetInput); | ||||
|     const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts)); | ||||
|     // Sort fill arrays by descending adjusted completed rate. | ||||
|     const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate())); | ||||
|     if (sortedPaths.length === 0) { | ||||
|         return undefined; | ||||
|     } | ||||
|     let optimalPath = sortedPaths[0]; | ||||
|     for (const [i, path] of sortedPaths.slice(1).entries()) { | ||||
|         optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i); | ||||
|         optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates); | ||||
|         // Yield to event loop. | ||||
|         await Promise.resolve(); | ||||
|     } | ||||
|     return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; | ||||
|     return optimalPath.isComplete() ? optimalPath : undefined; | ||||
| } | ||||
|  | ||||
| function mixPaths( | ||||
|     side: MarketOperation, | ||||
|     pathA: Fill[], | ||||
|     pathB: Fill[], | ||||
|     pathA: Path, | ||||
|     pathB: Path, | ||||
|     targetInput: BigNumber, | ||||
|     maxSteps: number, | ||||
| ): Fill[] { | ||||
|     rates: { [id: string]: BigNumber }, | ||||
| ): Path { | ||||
|     const _maxSteps = Math.max(maxSteps, 32); | ||||
|     let steps = 0; | ||||
|     // We assume pathA is the better of the two initially. | ||||
|     let bestPath: Fill[] = pathA; | ||||
|     let [bestPathInput, bestPathOutput] = getPathAdjustedSize(pathA, targetInput); | ||||
|     let bestPathRate = getCompleteRate(side, bestPathInput, bestPathOutput, targetInput); | ||||
|     const _isBetterPath = (input: BigNumber, rate: BigNumber) => { | ||||
|         if (bestPathInput.lt(targetInput)) { | ||||
|             return input.gt(bestPathInput); | ||||
|         } else if (input.gte(targetInput)) { | ||||
|             return rate.gt(bestPathRate); | ||||
|         } | ||||
|         return false; | ||||
|     }; | ||||
|     const _walk = (path: Fill[], input: BigNumber, output: BigNumber, flags: number, remainingFills: Fill[]) => { | ||||
|     let bestPath: Path = pathA; | ||||
|  | ||||
|     const _walk = (path: Path, remainingFills: Fill[]) => { | ||||
|         steps += 1; | ||||
|         const rate = getCompleteRate(side, input, output, targetInput); | ||||
|         if (_isBetterPath(input, rate)) { | ||||
|         if (path.isBetterThan(bestPath)) { | ||||
|             bestPath = path; | ||||
|             bestPathInput = input; | ||||
|             bestPathOutput = output; | ||||
|             bestPathRate = rate; | ||||
|         } | ||||
|         const remainingInput = targetInput.minus(input); | ||||
|         if (remainingInput.gt(0)) { | ||||
|         const remainingInput = targetInput.minus(path.size().input); | ||||
|         if (remainingInput.isGreaterThan(0)) { | ||||
|             for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) { | ||||
|                 const fill = remainingFills[i]; | ||||
|                 // Only walk valid paths. | ||||
|                 if (!isValidNextPathFill(path, flags, fill)) { | ||||
|                 if (!path.isValidNextFill(fill)) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 // Remove this fill from the next list of candidate fills. | ||||
|                 const nextRemainingFills = remainingFills.slice(); | ||||
|                 nextRemainingFills.splice(i, 1); | ||||
|                 // Recurse. | ||||
|                 _walk( | ||||
|                     [...path, fill], | ||||
|                     input.plus(BigNumber.min(remainingInput, fill.input)), | ||||
|                     output.plus( | ||||
|                         // Clip the output of the next fill to the remaining | ||||
|                         // input. | ||||
|                         clipFillAdjustedOutput(fill, remainingInput), | ||||
|                     ), | ||||
|                     flags | fill.flags, | ||||
|                     nextRemainingFills, | ||||
|                 ); | ||||
|                 _walk(Path.clone(path).append(fill), nextRemainingFills); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     const allFills = [...pathA, ...pathB]; | ||||
|     const sources = allFills.filter(f => f.index === 0).map(f => f.sourcePathId); | ||||
|     const rateBySource = Object.assign( | ||||
|         {}, | ||||
|         ...sources.map(s => ({ | ||||
|             [s]: getPathAdjustedRate(side, allFills.filter(f => f.sourcePathId === s), targetInput), | ||||
|         })), | ||||
|     ); | ||||
|     const allFills = [...pathA.fills, ...pathB.fills]; | ||||
|     // Sort subpaths by rate and keep fills contiguous to improve our | ||||
|     // chances of walking ideal, valid paths first. | ||||
|     const sortedFills = allFills.sort((a, b) => { | ||||
|         if (a.sourcePathId !== b.sourcePathId) { | ||||
|             return rateBySource[b.sourcePathId].comparedTo(rateBySource[a.sourcePathId]); | ||||
|             return rates[b.sourcePathId].comparedTo(rates[a.sourcePathId]); | ||||
|         } | ||||
|         return a.index - b.index; | ||||
|     }); | ||||
|     _walk([], ZERO_AMOUNT, ZERO_AMOUNT, 0, sortedFills); | ||||
|     if (!isValidPath(bestPath)) { | ||||
|     _walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills); | ||||
|     if (!bestPath.isValid()) { | ||||
|         throw new Error('nooope'); | ||||
|     } | ||||
|     return bestPath; | ||||
| } | ||||
|  | ||||
| function isValidNextPathFill(path: Fill[], pathFlags: number, fill: Fill): boolean { | ||||
|     if (path.length === 0) { | ||||
|         return !fill.parent; | ||||
|     } | ||||
|     if (path[path.length - 1] === fill.parent) { | ||||
|         return true; | ||||
|     } | ||||
|     if (fill.parent) { | ||||
|         return false; | ||||
|     } | ||||
|     return arePathFlagsAllowed(pathFlags | fill.flags); | ||||
| } | ||||
|  | ||||
| function isPathComplete(path: Fill[], targetInput: BigNumber): boolean { | ||||
|     const [input] = getPathSize(path); | ||||
|     return input.gte(targetInput); | ||||
| } | ||||
|  | ||||
| function clipFillAdjustedOutput(fill: Fill, remainingInput: BigNumber): BigNumber { | ||||
|     if (fill.input.lte(remainingInput)) { | ||||
|         return fill.adjustedOutput; | ||||
|     } | ||||
|     // Penalty does not get interpolated. | ||||
|     const penalty = fill.adjustedOutput.minus(fill.output); | ||||
|     return remainingInput.times(fill.output.div(fill.input)).plus(penalty); | ||||
| function rateBySourcePathId( | ||||
|     side: MarketOperation, | ||||
|     fills: Fill[][], | ||||
|     targetInput: BigNumber, | ||||
| ): { [id: string]: BigNumber } { | ||||
|     const flattenedFills = _.flatten(fills); | ||||
|     const sourcePathIds = flattenedFills.filter(f => f.index === 0).map(f => f.sourcePathId); | ||||
|     return Object.assign( | ||||
|         {}, | ||||
|         ...sourcePathIds.map(s => ({ | ||||
|             [s]: Path.create(side, flattenedFills.filter(f => f.sourcePathId === s), targetInput).adjustedRate(), | ||||
|         })), | ||||
|     ); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| import { BigNumber } from '@0x/utils'; | ||||
|  | ||||
| import { MarketOperation } from '../../types'; | ||||
|  | ||||
| import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; | ||||
| import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types'; | ||||
|  | ||||
| /** | ||||
|  * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the | ||||
|  * quote falls short of the target input. | ||||
|  */ | ||||
| export function getTwoHopAdjustedRate( | ||||
|     side: MarketOperation, | ||||
|     twoHopQuote: DexSample<MultiHopFillData>, | ||||
|     targetInput: BigNumber, | ||||
|     ethToOutputRate: BigNumber, | ||||
|     fees: FeeSchedule = {}, | ||||
|     exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, | ||||
| ): BigNumber { | ||||
|     const { output, input, fillData } = twoHopQuote; | ||||
|     if (input.isLessThan(targetInput) || output.isZero()) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     const penalty = ethToOutputRate.times( | ||||
|         exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), | ||||
|     ); | ||||
|     const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||
|     return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Computes the "complete" rate given the input/output of a path. | ||||
|  * This value penalizes the path if it falls short of the target input. | ||||
|  */ | ||||
| export function getCompleteRate( | ||||
|     side: MarketOperation, | ||||
|     input: BigNumber, | ||||
|     output: BigNumber, | ||||
|     targetInput: BigNumber, | ||||
| ): BigNumber { | ||||
|     if (input.eq(0) || output.eq(0) || targetInput.eq(0)) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     // Penalize paths that fall short of the entire input amount by a factor of | ||||
|     // input / targetInput => (i / t) | ||||
|     if (side === MarketOperation.Sell) { | ||||
|         // (o / i) * (i / t) => (o / t) | ||||
|         return output.div(targetInput); | ||||
|     } | ||||
|     // (i / o) * (i / t) | ||||
|     return input.div(output).times(input.div(targetInput)); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Computes the rate given the input/output of a path. | ||||
|  */ | ||||
| export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { | ||||
|     if (input.eq(0) || output.eq(0)) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     return side === MarketOperation.Sell ? output.div(input) : input.div(output); | ||||
| } | ||||
| @@ -734,6 +734,32 @@ export class SamplerOperations { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public getShellSellQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): SourceQuoteOperation { | ||||
|         return new SamplerContractOperation({ | ||||
|             source: ERC20BridgeSource.Shell, | ||||
|             contract: this._samplerContract, | ||||
|             function: this._samplerContract.sampleSellsFromShell, | ||||
|             params: [takerToken, makerToken, takerFillAmounts], | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public getShellBuyQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|     ): SourceQuoteOperation { | ||||
|         return new SamplerContractOperation({ | ||||
|             source: ERC20BridgeSource.Shell, | ||||
|             contract: this._samplerContract, | ||||
|             function: this._samplerContract.sampleBuysFromShell, | ||||
|             params: [takerToken, makerToken, makerFillAmounts], | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public getMedianSellRate( | ||||
|         sources: ERC20BridgeSource[], | ||||
|         makerToken: string, | ||||
| @@ -971,6 +997,8 @@ export class SamplerOperations { | ||||
|                                 .map(poolAddress => | ||||
|                                     this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts), | ||||
|                                 ); | ||||
|                         case ERC20BridgeSource.Shell: | ||||
|                             return this.getShellSellQuotes(makerToken, takerToken, takerFillAmounts); | ||||
|                         default: | ||||
|                             throw new Error(`Unsupported sell sample source: ${source}`); | ||||
|                     } | ||||
| @@ -1058,6 +1086,8 @@ export class SamplerOperations { | ||||
|                                 .map(poolAddress => | ||||
|                                     this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts), | ||||
|                                 ); | ||||
|                         case ERC20BridgeSource.Shell: | ||||
|                             return this.getShellBuyQuotes(makerToken, takerToken, makerFillAmounts); | ||||
|                         default: | ||||
|                             throw new Error(`Unsupported buy sample source: ${source}`); | ||||
|                     } | ||||
|   | ||||
| @@ -41,6 +41,7 @@ export enum ERC20BridgeSource { | ||||
|     MStable = 'mStable', | ||||
|     Mooniswap = 'Mooniswap', | ||||
|     MultiHop = 'MultiHop', | ||||
|     Shell = 'Shell', | ||||
|     Swerve = 'Swerve', | ||||
|     SushiSwap = 'SushiSwap', | ||||
| } | ||||
| @@ -156,16 +157,6 @@ export interface DexSample<TFillData extends FillData = FillData> extends Source | ||||
|     output: BigNumber; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Flags for `Fill` objects. | ||||
|  */ | ||||
| export enum FillFlags { | ||||
|     ConflictsWithKyber = 0x1, | ||||
|     Kyber = 0x2, | ||||
|     ConflictsWithMultiBridge = 0x4, | ||||
|     MultiBridge = 0x8, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Represents a node on a fill path. | ||||
|  */ | ||||
| @@ -174,8 +165,8 @@ export interface Fill<TFillData extends FillData = FillData> extends SourceInfo< | ||||
|     // This is generated when the path is generated and is useful to distinguish | ||||
|     // paths that have the same `source` IDs but are distinct (e.g., Curves). | ||||
|     sourcePathId: string; | ||||
|     // See `FillFlags`. | ||||
|     flags: FillFlags; | ||||
|     // See `SOURCE_FLAGS`. | ||||
|     flags: number; | ||||
|     // Input fill amount (taker asset amount in a sell, maker asset amount in a buy). | ||||
|     input: BigNumber; | ||||
|     // Output fill amount (maker asset amount in a sell, taker asset amount in a buy). | ||||
| @@ -234,6 +225,7 @@ export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts { | ||||
|  | ||||
| export type FeeEstimate = (fillData?: FillData) => number | BigNumber; | ||||
| export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; | ||||
| export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber; | ||||
|  | ||||
| /** | ||||
|  * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. | ||||
| @@ -288,17 +280,13 @@ export interface GetMarketOrdersOpts { | ||||
|      * Estimated gas consumed by each liquidity source. | ||||
|      */ | ||||
|     gasSchedule: FeeSchedule; | ||||
|     exchangeProxyOverhead: ExchangeProxyOverhead; | ||||
|     /** | ||||
|      * Whether to pad the quote with a redundant fallback quote using different | ||||
|      * sources. Defaults to `true`. | ||||
|      */ | ||||
|     allowFallback: boolean; | ||||
|     rfqt?: GetMarketOrdersRfqtOpts; | ||||
|     /** | ||||
|      * Whether to combine contiguous bridge orders into a single DexForwarderBridge | ||||
|      * order. Defaults to `true`. | ||||
|      */ | ||||
|     shouldBatchBridgeOrders: boolean; | ||||
|     /** | ||||
|      * Whether to generate a quote report | ||||
|      */ | ||||
| @@ -321,7 +309,8 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData> | ||||
|  | ||||
| export interface OptimizerResult { | ||||
|     optimizedOrders: OptimizedMarketOrder[]; | ||||
|     isTwoHop: boolean; | ||||
|     sourceFlags: number; | ||||
|     liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; | ||||
|     quoteReport?: QuoteReport; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,7 @@ export function generateQuoteReport( | ||||
|     multiHopQuotes: Array<DexSample<MultiHopFillData>>, | ||||
|     nativeOrders: SignedOrder[], | ||||
|     orderFillableAmounts: BigNumber[], | ||||
|     liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>, | ||||
|     liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>, | ||||
|     quoteRequestor?: QuoteRequestor, | ||||
| ): QuoteReport { | ||||
|     const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation)); | ||||
| @@ -101,7 +101,9 @@ export function generateQuoteReport( | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)]; | ||||
|         sourcesDelivered = [ | ||||
|             _multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation), | ||||
|         ]; | ||||
|     } | ||||
|     return { | ||||
|         sourcesConsidered, | ||||
|   | ||||
| @@ -2,13 +2,13 @@ import { schemas, SchemaValidator } from '@0x/json-schemas'; | ||||
| import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; | ||||
| import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; | ||||
| import { ERC20AssetData } from '@0x/types'; | ||||
| import { BigNumber, logUtils } from '@0x/utils'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import Axios, { AxiosInstance } from 'axios'; | ||||
| import { Agent as HttpAgent } from 'http'; | ||||
| import { Agent as HttpsAgent } from 'https'; | ||||
|  | ||||
| import { constants } from '../constants'; | ||||
| import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; | ||||
| import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; | ||||
|  | ||||
| import { ONE_SECOND_MS } from './market_operation_utils/constants'; | ||||
| import { RfqMakerBlacklist } from './rfq_maker_blacklist'; | ||||
| @@ -107,20 +107,18 @@ function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has Axi | ||||
|     } | ||||
| } | ||||
|  | ||||
| export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; | ||||
|  | ||||
| export class QuoteRequestor { | ||||
|     private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); | ||||
|     private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {}; | ||||
|  | ||||
|     constructor( | ||||
|         private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, | ||||
|         private readonly _warningLogger: LogFunction = (obj, msg) => | ||||
|             logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), | ||||
|         private readonly _infoLogger: LogFunction = (obj, msg) => | ||||
|             logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), | ||||
|         private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER, | ||||
|         private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER, | ||||
|         private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs, | ||||
|     ) {} | ||||
|     ) { | ||||
|         rfqMakerBlacklist.infoLogger = this._infoLogger; | ||||
|     } | ||||
|  | ||||
|     public async requestRfqtFirmQuotesAsync( | ||||
|         makerAssetData: string, | ||||
| @@ -336,13 +334,6 @@ export class QuoteRequestor { | ||||
|         options: RfqtRequestOpts, | ||||
|         quoteType: 'firm' | 'indicative', | ||||
|     ): Promise<Array<{ response: ResponseT; makerUri: string }>> { | ||||
|         const result: Array<{ response: ResponseT; makerUri: string }> = []; | ||||
|         await Promise.all( | ||||
|             Object.keys(this._rfqtAssetOfferings).map(async url => { | ||||
|                 if ( | ||||
|                     this._makerSupportsPair(url, makerAssetData, takerAssetData) && | ||||
|                     !rfqMakerBlacklist.isMakerBlacklisted(url) | ||||
|                 ) { | ||||
|         const requestParamsWithBigNumbers = { | ||||
|             takerAddress: options.takerAddress, | ||||
|             ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), | ||||
| @@ -360,7 +351,14 @@ export class QuoteRequestor { | ||||
|                 : undefined, | ||||
|         }; | ||||
|  | ||||
|                     const partialLogEntry = { url, quoteType, requestParams }; | ||||
|         const result: Array<{ response: ResponseT; makerUri: string }> = []; | ||||
|         await Promise.all( | ||||
|             Object.keys(this._rfqtAssetOfferings).map(async url => { | ||||
|                 const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url); | ||||
|                 const partialLogEntry = { url, quoteType, requestParams, isBlacklisted }; | ||||
|                 if (isBlacklisted) { | ||||
|                     this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } }); | ||||
|                 } else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { | ||||
|                     const timeBeforeAwait = Date.now(); | ||||
|                     const maxResponseTimeMs = | ||||
|                         options.makerEndpointMaxResponseTimeMs === undefined | ||||
| @@ -395,7 +393,7 @@ export class QuoteRequestor { | ||||
|                                 }, | ||||
|                             }, | ||||
|                         }); | ||||
|                         rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); | ||||
|                         rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); | ||||
|                         result.push({ response: response.data, makerUri: url }); | ||||
|                     } catch (err) { | ||||
|                         const latencyMs = Date.now() - timeBeforeAwait; | ||||
| @@ -411,7 +409,7 @@ export class QuoteRequestor { | ||||
|                                 }, | ||||
|                             }, | ||||
|                         }); | ||||
|                         rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); | ||||
|                         rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); | ||||
|                         this._warningLogger( | ||||
|                             convertIfAxiosError(err), | ||||
|                             `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ | ||||
|   | ||||
| @@ -349,7 +349,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { | ||||
| function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { | ||||
|     const fills: CollapsedFill[] = []; | ||||
|     for (const o of orders) { | ||||
|         fills.push(...o.fills); | ||||
|   | ||||
| @@ -4,11 +4,16 @@ | ||||
|  */ | ||||
|  | ||||
| import { constants } from '../constants'; | ||||
| import { LogFunction } from '../types'; | ||||
|  | ||||
| export class RfqMakerBlacklist { | ||||
|     private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {}; | ||||
|     private readonly _makerBlacklistedUntilDate: { [makerUrl: string]: number } = {}; | ||||
|     constructor(private readonly _blacklistDurationMinutes: number, private readonly _timeoutStreakThreshold: number) {} | ||||
|     constructor( | ||||
|         private readonly _blacklistDurationMinutes: number, | ||||
|         private readonly _timeoutStreakThreshold: number, | ||||
|         public infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER, | ||||
|     ) {} | ||||
|     public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void { | ||||
|         if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) { | ||||
|             this._makerTimeoutStreakLength[makerUrl] = 0; | ||||
| @@ -16,8 +21,12 @@ export class RfqMakerBlacklist { | ||||
|         if (didTimeout) { | ||||
|             this._makerTimeoutStreakLength[makerUrl] += 1; | ||||
|             if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) { | ||||
|                 this._makerBlacklistedUntilDate[makerUrl] = | ||||
|                     Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; | ||||
|                 const blacklistEnd = Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; | ||||
|                 this._makerBlacklistedUntilDate[makerUrl] = blacklistEnd; | ||||
|                 this.infoLogger( | ||||
|                     { makerUrl, blacklistedUntil: new Date(blacklistEnd).toISOString() }, | ||||
|                     'maker blacklisted', | ||||
|                 ); | ||||
|             } | ||||
|         } else { | ||||
|             this._makerTimeoutStreakLength[makerUrl] = 0; | ||||
| @@ -27,6 +36,7 @@ export class RfqMakerBlacklist { | ||||
|         const now = Date.now(); | ||||
|         if (now > this._makerBlacklistedUntilDate[makerUrl]) { | ||||
|             delete this._makerBlacklistedUntilDate[makerUrl]; | ||||
|             this.infoLogger({ makerUrl }, 'maker unblacklisted'); | ||||
|         } | ||||
|         return this._makerBlacklistedUntilDate[makerUrl] > now; | ||||
|     } | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import { | ||||
| } from '../types'; | ||||
|  | ||||
| import { MarketOperationUtils } from './market_operation_utils'; | ||||
| import { SOURCE_FLAGS } from './market_operation_utils/constants'; | ||||
| import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; | ||||
| import { | ||||
|     ERC20BridgeSource, | ||||
| @@ -24,10 +25,9 @@ import { | ||||
|     GetMarketOrdersOpts, | ||||
|     OptimizedMarketOrder, | ||||
| } from './market_operation_utils/types'; | ||||
| import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; | ||||
|  | ||||
| import { QuoteReport } from './quote_report_generator'; | ||||
| import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; | ||||
| import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; | ||||
|  | ||||
| // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? | ||||
| export class SwapQuoteCalculator { | ||||
| @@ -130,15 +130,15 @@ export class SwapQuoteCalculator { | ||||
|  | ||||
|         let optimizedOrders: OptimizedMarketOrder[]; | ||||
|         let quoteReport: QuoteReport | undefined; | ||||
|         let isTwoHop = false; | ||||
|         let sourceFlags: number = 0; | ||||
|  | ||||
|         { | ||||
|         // Scale fees by gas price. | ||||
|         const _opts: GetMarketOrdersOpts = { | ||||
|             ...opts, | ||||
|             feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => | ||||
|                 gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), | ||||
|             ), | ||||
|             exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), | ||||
|         }; | ||||
|  | ||||
|         const firstOrderMakerAssetData = !!prunedOrders[0] | ||||
| @@ -157,7 +157,7 @@ export class SwapQuoteCalculator { | ||||
|                 ); | ||||
|                 optimizedOrders = buyResult.optimizedOrders; | ||||
|                 quoteReport = buyResult.quoteReport; | ||||
|                     isTwoHop = buyResult.isTwoHop; | ||||
|                 sourceFlags = buyResult.sourceFlags; | ||||
|             } else { | ||||
|                 const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     prunedOrders, | ||||
| @@ -166,14 +166,14 @@ export class SwapQuoteCalculator { | ||||
|                 ); | ||||
|                 optimizedOrders = sellResult.optimizedOrders; | ||||
|                 quoteReport = sellResult.quoteReport; | ||||
|                     isTwoHop = sellResult.isTwoHop; | ||||
|                 } | ||||
|                 sourceFlags = sellResult.sourceFlags; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // assetData information for the result | ||||
|         const { makerAssetData, takerAssetData } = prunedOrders[0]; | ||||
|         return isTwoHop | ||||
|         const swapQuote = | ||||
|             sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop] | ||||
|                 ? createTwoHopSwapQuote( | ||||
|                       makerAssetData, | ||||
|                       takerAssetData, | ||||
| @@ -194,6 +194,11 @@ export class SwapQuoteCalculator { | ||||
|                       opts.gasSchedule, | ||||
|                       quoteReport, | ||||
|                   ); | ||||
|         // Use the raw gas, not scaled by gas price | ||||
|         const exchangeProxyOverhead = opts.exchangeProxyOverhead(sourceFlags).toNumber(); | ||||
|         swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead; | ||||
|         swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead; | ||||
|         return swapQuote; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquid | ||||
| import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; | ||||
| import * as IMStable from '../test/generated-artifacts/IMStable.json'; | ||||
| import * as IMultiBridge from '../test/generated-artifacts/IMultiBridge.json'; | ||||
| import * as IShell from '../test/generated-artifacts/IShell.json'; | ||||
| import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; | ||||
| import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; | ||||
| import * as KyberSampler from '../test/generated-artifacts/KyberSampler.json'; | ||||
| @@ -30,6 +31,7 @@ import * as MStableSampler from '../test/generated-artifacts/MStableSampler.json | ||||
| import * as MultiBridgeSampler from '../test/generated-artifacts/MultiBridgeSampler.json'; | ||||
| import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSampler.json'; | ||||
| import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json'; | ||||
| import * as ShellSampler from '../test/generated-artifacts/ShellSampler.json'; | ||||
| import * as SushiSwapSampler from '../test/generated-artifacts/SushiSwapSampler.json'; | ||||
| import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; | ||||
| import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json'; | ||||
| @@ -50,6 +52,7 @@ export const artifacts = { | ||||
|     MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, | ||||
|     NativeOrderSampler: NativeOrderSampler as ContractArtifact, | ||||
|     SamplerUtils: SamplerUtils as ContractArtifact, | ||||
|     ShellSampler: ShellSampler as ContractArtifact, | ||||
|     SushiSwapSampler: SushiSwapSampler as ContractArtifact, | ||||
|     TwoHopSampler: TwoHopSampler as ContractArtifact, | ||||
|     UniswapSampler: UniswapSampler as ContractArtifact, | ||||
| @@ -62,6 +65,7 @@ export const artifacts = { | ||||
|     ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, | ||||
|     IMStable: IMStable as ContractArtifact, | ||||
|     IMultiBridge: IMultiBridge as ContractArtifact, | ||||
|     IShell: IShell as ContractArtifact, | ||||
|     IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, | ||||
|     IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, | ||||
|     DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, | ||||
|   | ||||
| @@ -1,289 +0,0 @@ | ||||
| import { ContractAddresses } from '@0x/contract-addresses'; | ||||
| import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers'; | ||||
| import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; | ||||
| import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; | ||||
| import { migrateOnceAsync } from '@0x/migrations'; | ||||
| import { assetDataUtils } from '@0x/order-utils'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import * as chai from 'chai'; | ||||
| import 'mocha'; | ||||
|  | ||||
| import { SwapQuote } from '../src'; | ||||
| import { constants } from '../src/constants'; | ||||
| import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer'; | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; | ||||
|  | ||||
| import { chaiSetup } from './utils/chai_setup'; | ||||
| import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; | ||||
| import { provider, web3Wrapper } from './utils/web3_wrapper'; | ||||
|  | ||||
| chaiSetup.configure(); | ||||
| const expect = chai.expect; | ||||
| const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); | ||||
|  | ||||
| const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); | ||||
| const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); | ||||
| const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; | ||||
| const UNLIMITED_ALLOWANCE = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers | ||||
|  | ||||
| const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [ | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
| ]; | ||||
|  | ||||
| const expectMakerAndTakerBalancesAsyncFactory = ( | ||||
|     erc20TokenContract: ERC20TokenContract, | ||||
|     makerAddress: string, | ||||
|     takerAddress: string, | ||||
| ) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => { | ||||
|     const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); | ||||
|     const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); | ||||
|     expect(makerBalance).to.bignumber.equal(expectedMakerBalance); | ||||
|     expect(takerBalance).to.bignumber.equal(expectedTakerBalance); | ||||
| }; | ||||
|  | ||||
| describe('ExchangeSwapQuoteConsumer', () => { | ||||
|     let userAddresses: string[]; | ||||
|     let erc20MakerTokenContract: ERC20TokenContract; | ||||
|     let erc20TakerTokenContract: ERC20TokenContract; | ||||
|     let coinbaseAddress: string; | ||||
|     let makerAddress: string; | ||||
|     let takerAddress: string; | ||||
|     let orderFactory: OrderFactory; | ||||
|     let feeRecipient: string; | ||||
|     let makerTokenAddress: string; | ||||
|     let takerTokenAddress: string; | ||||
|     let makerAssetData: string; | ||||
|     let takerAssetData: string; | ||||
|     let contractAddresses: ContractAddresses; | ||||
|     let exchangeContract: ExchangeContract; | ||||
|  | ||||
|     const chainId = TESTRPC_CHAIN_ID; | ||||
|  | ||||
|     let orders: SignedOrderWithFillableAmounts[]; | ||||
|     let marketSellSwapQuote: SwapQuote; | ||||
|     let marketBuySwapQuote: SwapQuote; | ||||
|     let swapQuoteConsumer: ExchangeSwapQuoteConsumer; | ||||
|     let expectMakerAndTakerBalancesForMakerAssetAsync: ( | ||||
|         expectedMakerBalance: BigNumber, | ||||
|         expectedTakerBalance: BigNumber, | ||||
|     ) => Promise<void>; | ||||
|     let expectMakerAndTakerBalancesForTakerAssetAsync: ( | ||||
|         expectedMakerBalance: BigNumber, | ||||
|         expectedTakerBalance: BigNumber, | ||||
|     ) => Promise<void>; | ||||
|  | ||||
|     before(async () => { | ||||
|         contractAddresses = await migrateOnceAsync(provider); | ||||
|         await blockchainLifecycle.startAsync(); | ||||
|         userAddresses = await web3Wrapper.getAvailableAddressesAsync(); | ||||
|         [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; | ||||
|         [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); | ||||
|         [makerAssetData, takerAssetData] = [ | ||||
|             assetDataUtils.encodeERC20AssetData(makerTokenAddress), | ||||
|             assetDataUtils.encodeERC20AssetData(takerTokenAddress), | ||||
|         ]; | ||||
|         erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider); | ||||
|         erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider); | ||||
|         exchangeContract = new ExchangeContract(contractAddresses.exchange, provider); | ||||
|         // Configure order defaults | ||||
|         const defaultOrderParams = { | ||||
|             ...devConstants.STATIC_ORDER_PARAMS, | ||||
|             makerAddress, | ||||
|             takerAddress, | ||||
|             makerAssetData, | ||||
|             takerAssetData, | ||||
|             makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, | ||||
|             takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, | ||||
|             makerFee: constants.ZERO_AMOUNT, | ||||
|             takerFee: constants.ZERO_AMOUNT, | ||||
|             feeRecipientAddress: feeRecipient, | ||||
|             exchangeAddress: contractAddresses.exchange, | ||||
|             chainId, | ||||
|         }; | ||||
|         const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; | ||||
|         orderFactory = new OrderFactory(privateKey, defaultOrderParams); | ||||
|         expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( | ||||
|             erc20TakerTokenContract, | ||||
|             makerAddress, | ||||
|             takerAddress, | ||||
|         ); | ||||
|         expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( | ||||
|             erc20MakerTokenContract, | ||||
|             makerAddress, | ||||
|             takerAddress, | ||||
|         ); | ||||
|     }); | ||||
|     after(async () => { | ||||
|         await blockchainLifecycle.revertAsync(); | ||||
|     }); | ||||
|     beforeEach(async () => { | ||||
|         await blockchainLifecycle.startAsync(); | ||||
|         orders = []; | ||||
|         for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { | ||||
|             const order = await orderFactory.newSignedOrderAsync(partialOrder); | ||||
|             const prunedOrder = { | ||||
|                 ...order, | ||||
|                 ...partialOrder, | ||||
|             }; | ||||
|             orders.push(prunedOrder as SignedOrderWithFillableAmounts); | ||||
|         } | ||||
|  | ||||
|         marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|             makerAssetData, | ||||
|             takerAssetData, | ||||
|             orders, | ||||
|             MarketOperation.Sell, | ||||
|             GAS_PRICE, | ||||
|         ); | ||||
|  | ||||
|         marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|             makerAssetData, | ||||
|             takerAssetData, | ||||
|             orders, | ||||
|             MarketOperation.Buy, | ||||
|             GAS_PRICE, | ||||
|         ); | ||||
|  | ||||
|         swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, { | ||||
|             chainId, | ||||
|         }); | ||||
|  | ||||
|         await erc20MakerTokenContract | ||||
|             .transfer(makerAddress, marketBuySwapQuote.worstCaseQuoteInfo.makerAssetAmount) | ||||
|             .sendTransactionAsync({ | ||||
|                 from: coinbaseAddress, | ||||
|             }); | ||||
|         await erc20TakerTokenContract | ||||
|             .transfer(takerAddress, marketBuySwapQuote.worstCaseQuoteInfo.totalTakerAssetAmount) | ||||
|             .sendTransactionAsync({ | ||||
|                 from: coinbaseAddress, | ||||
|             }); | ||||
|         await erc20MakerTokenContract | ||||
|             .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) | ||||
|             .sendTransactionAsync({ from: makerAddress }); | ||||
|         await erc20TakerTokenContract | ||||
|             .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) | ||||
|             .sendTransactionAsync({ from: takerAddress }); | ||||
|     }); | ||||
|     afterEach(async () => { | ||||
|         await blockchainLifecycle.revertAsync(); | ||||
|     }); | ||||
|     describe('#executeSwapQuoteOrThrowAsync', () => { | ||||
|         /* | ||||
|          * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) | ||||
|          * Does not test the validity of the state change performed by the forwarder smart contract | ||||
|          */ | ||||
|         it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => { | ||||
|             await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 constants.ZERO_AMOUNT, | ||||
|             ); | ||||
|             await expectMakerAndTakerBalancesForTakerAssetAsync( | ||||
|                 constants.ZERO_AMOUNT, | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|             ); | ||||
|             await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { | ||||
|                 takerAddress, | ||||
|                 gasLimit: 4000000, | ||||
|             }); | ||||
|             await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                 constants.ZERO_AMOUNT, | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|             ); | ||||
|             await expectMakerAndTakerBalancesForTakerAssetAsync( | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 constants.ZERO_AMOUNT, | ||||
|             ); | ||||
|         }); | ||||
|         it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { | ||||
|             await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 constants.ZERO_AMOUNT, | ||||
|             ); | ||||
|             await expectMakerAndTakerBalancesForTakerAssetAsync( | ||||
|                 constants.ZERO_AMOUNT, | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|             ); | ||||
|             await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { | ||||
|                 takerAddress, | ||||
|                 gasLimit: 4000000, | ||||
|             }); | ||||
|             await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                 constants.ZERO_AMOUNT, | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|             ); | ||||
|             await expectMakerAndTakerBalancesForTakerAssetAsync( | ||||
|                 new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 constants.ZERO_AMOUNT, | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('#getCalldataOrThrow', () => { | ||||
|         describe('valid swap quote', async () => { | ||||
|             it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { | ||||
|                 await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketSellSwapQuote, | ||||
|                     {}, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(exchangeContract.address); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     gas: 4000000, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     value: ethAmount, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|             it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { | ||||
|                 await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketBuySwapQuote, | ||||
|                     {}, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(exchangeContract.address); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     gas: 4000000, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     value: ethAmount, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesForMakerAssetAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,440 +0,0 @@ | ||||
| import { ContractAddresses } from '@0x/contract-addresses'; | ||||
| import { ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers'; | ||||
| import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; | ||||
| import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; | ||||
| import { migrateOnceAsync } from '@0x/migrations'; | ||||
| import { assetDataUtils } from '@0x/order-utils'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import * as chai from 'chai'; | ||||
| import 'mocha'; | ||||
|  | ||||
| import { SwapQuote } from '../src'; | ||||
| import { constants } from '../src/constants'; | ||||
| import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer'; | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; | ||||
|  | ||||
| import { chaiSetup } from './utils/chai_setup'; | ||||
| import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; | ||||
| import { provider, web3Wrapper } from './utils/web3_wrapper'; | ||||
|  | ||||
| chaiSetup.configure(); | ||||
| const expect = chai.expect; | ||||
| const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); | ||||
|  | ||||
| const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); | ||||
| const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); | ||||
| const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; | ||||
|  | ||||
| const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers | ||||
| const FEE_PERCENTAGE = 0.05; | ||||
| const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [ | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
|     { | ||||
|         takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), | ||||
|         fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), | ||||
|     }, | ||||
| ]; | ||||
|  | ||||
| const expectMakerAndTakerBalancesAsyncFactory = ( | ||||
|     erc20TokenContract: ERC20TokenContract, | ||||
|     makerAddress: string, | ||||
|     takerAddress: string, | ||||
| ) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => { | ||||
|     const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); | ||||
|     const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); | ||||
|     expect(makerBalance).to.bignumber.equal(expectedMakerBalance); | ||||
|     expect(takerBalance).to.bignumber.equal(expectedTakerBalance); | ||||
| }; | ||||
|  | ||||
| describe('ForwarderSwapQuoteConsumer', () => { | ||||
|     let userAddresses: string[]; | ||||
|     let coinbaseAddress: string; | ||||
|     let makerAddress: string; | ||||
|     let takerAddress: string; | ||||
|     let feeRecipient: string; | ||||
|     let makerTokenAddress: string; | ||||
|     let takerTokenAddress: string; | ||||
|     let makerAssetData: string; | ||||
|     let takerAssetData: string; | ||||
|     let orderFactory: OrderFactory; | ||||
|     let invalidOrderFactory: OrderFactory; | ||||
|     let wethAssetData: string; | ||||
|     let contractAddresses: ContractAddresses; | ||||
|     let erc20TokenContract: ERC20TokenContract; | ||||
|     let forwarderContract: ForwarderContract; | ||||
|  | ||||
|     let orders: SignedOrderWithFillableAmounts[]; | ||||
|     let invalidOrders: SignedOrderWithFillableAmounts[]; | ||||
|     let marketSellSwapQuote: SwapQuote; | ||||
|     let marketBuySwapQuote: SwapQuote; | ||||
|     let invalidMarketBuySwapQuote: SwapQuote; | ||||
|     let swapQuoteConsumer: ForwarderSwapQuoteConsumer; | ||||
|     let expectMakerAndTakerBalancesAsync: ( | ||||
|         expectedMakerBalance: BigNumber, | ||||
|         expectedTakerBalance: BigNumber, | ||||
|     ) => Promise<void>; | ||||
|     const chainId = TESTRPC_CHAIN_ID; | ||||
|  | ||||
|     before(async () => { | ||||
|         contractAddresses = await migrateOnceAsync(provider); | ||||
|         await blockchainLifecycle.startAsync(); | ||||
|         userAddresses = await web3Wrapper.getAvailableAddressesAsync(); | ||||
|         [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; | ||||
|         [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); | ||||
|         erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider); | ||||
|         forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider); | ||||
|         [makerAssetData, takerAssetData, wethAssetData] = [ | ||||
|             assetDataUtils.encodeERC20AssetData(makerTokenAddress), | ||||
|             assetDataUtils.encodeERC20AssetData(takerTokenAddress), | ||||
|             assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken), | ||||
|         ]; | ||||
|         // Configure order defaults | ||||
|         const defaultOrderParams = { | ||||
|             ...devConstants.STATIC_ORDER_PARAMS, | ||||
|             makerAddress, | ||||
|             takerAddress: constants.NULL_ADDRESS, | ||||
|             makerAssetData, | ||||
|             takerAssetData: wethAssetData, | ||||
|             makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, | ||||
|             takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, | ||||
|             makerFee: constants.ZERO_AMOUNT, | ||||
|             takerFee: constants.ZERO_AMOUNT, | ||||
|             feeRecipientAddress: feeRecipient, | ||||
|             exchangeAddress: contractAddresses.exchange, | ||||
|             chainId, | ||||
|         }; | ||||
|         const invalidDefaultOrderParams = { | ||||
|             ...defaultOrderParams, | ||||
|             ...{ | ||||
|                 takerAssetData, | ||||
|             }, | ||||
|         }; | ||||
|         const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; | ||||
|         orderFactory = new OrderFactory(privateKey, defaultOrderParams); | ||||
|         expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory( | ||||
|             erc20TokenContract, | ||||
|             makerAddress, | ||||
|             takerAddress, | ||||
|         ); | ||||
|         invalidOrderFactory = new OrderFactory(privateKey, invalidDefaultOrderParams); | ||||
|     }); | ||||
|     after(async () => { | ||||
|         await blockchainLifecycle.revertAsync(); | ||||
|     }); | ||||
|     beforeEach(async () => { | ||||
|         await blockchainLifecycle.startAsync(); | ||||
|         const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS; | ||||
|  | ||||
|         const totalFillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI); | ||||
|  | ||||
|         await erc20TokenContract.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({ | ||||
|             from: coinbaseAddress, | ||||
|         }); | ||||
|  | ||||
|         await erc20TokenContract | ||||
|             .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) | ||||
|             .sendTransactionAsync({ from: makerAddress }); | ||||
|  | ||||
|         await forwarderContract.approveMakerAssetProxy(makerAssetData).sendTransactionAsync({ from: makerAddress }); | ||||
|  | ||||
|         orders = []; | ||||
|         for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { | ||||
|             const order = await orderFactory.newSignedOrderAsync(partialOrder); | ||||
|             const prunedOrder = { | ||||
|                 ...order, | ||||
|                 ...partialOrder, | ||||
|             }; | ||||
|             orders.push(prunedOrder as SignedOrderWithFillableAmounts); | ||||
|         } | ||||
|  | ||||
|         invalidOrders = []; | ||||
|         for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { | ||||
|             const order = await invalidOrderFactory.newSignedOrderAsync(partialOrder); | ||||
|             const prunedOrder = { | ||||
|                 ...order, | ||||
|                 ...partialOrder, | ||||
|             }; | ||||
|             invalidOrders.push(prunedOrder as SignedOrderWithFillableAmounts); | ||||
|         } | ||||
|  | ||||
|         marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|             makerAssetData, | ||||
|             wethAssetData, | ||||
|             orders, | ||||
|             MarketOperation.Sell, | ||||
|             GAS_PRICE, | ||||
|         ); | ||||
|  | ||||
|         marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|             makerAssetData, | ||||
|             wethAssetData, | ||||
|             orders, | ||||
|             MarketOperation.Buy, | ||||
|             GAS_PRICE, | ||||
|         ); | ||||
|  | ||||
|         invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|             makerAssetData, | ||||
|             takerAssetData, | ||||
|             invalidOrders, | ||||
|             MarketOperation.Buy, | ||||
|             GAS_PRICE, | ||||
|         ); | ||||
|  | ||||
|         swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, { | ||||
|             chainId, | ||||
|         }); | ||||
|         swapQuoteConsumer.buyQuoteSellAmountScalingFactor = 1; | ||||
|     }); | ||||
|     afterEach(async () => { | ||||
|         await blockchainLifecycle.revertAsync(); | ||||
|     }); | ||||
|     describe('#executeSwapQuoteOrThrowAsync', () => { | ||||
|         describe('validation', () => { | ||||
|             it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth)', async () => { | ||||
|                 expect( | ||||
|                     swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }), | ||||
|                 ).to.be.rejectedWith( | ||||
|                     `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, | ||||
|                 ); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // TODO(david) test execution of swap quotes with fee orders | ||||
|         describe('valid swap quote', () => { | ||||
|             /* | ||||
|              * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) | ||||
|              * Does not test the validity of the state change performed by the forwarder smart contract | ||||
|              */ | ||||
|             it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { | ||||
|                     takerAddress, | ||||
|                     gasLimit: 4000000, | ||||
|                     ethAmount: new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { | ||||
|                     takerAddress, | ||||
|                     gasLimit: 4000000, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it('should perform a marketBuy execution with affiliate fees', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { | ||||
|                     takerAddress, | ||||
|                     gasLimit: 4000000, | ||||
|                     extensionContractOpts: { | ||||
|                         feePercentage: 0.05, | ||||
|                         feeRecipient, | ||||
|                     }, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( | ||||
|                     marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, | ||||
|                 ); | ||||
|                 expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( | ||||
|                     new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it('should perform a marketSell execution with affiliate fees', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { | ||||
|                     takerAddress, | ||||
|                     gasLimit: 4000000, | ||||
|                     extensionContractOpts: { | ||||
|                         feePercentage: 0.05, | ||||
|                         feeRecipient, | ||||
|                     }, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( | ||||
|                     marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, | ||||
|                 ); | ||||
|                 expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( | ||||
|                     new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), | ||||
|                 ); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('#getCalldataOrThrow', () => { | ||||
|         describe('validation', () => { | ||||
|             it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { | ||||
|                 expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith( | ||||
|                     `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, | ||||
|                 ); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('valid swap quote', async () => { | ||||
|             it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketSellSwapQuote, | ||||
|                     {}, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(forwarderContract.address); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     value: ethAmount, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     gas: 4000000, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|             it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketBuySwapQuote, | ||||
|                     {}, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(contractAddresses.forwarder); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     value: ethAmount, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     gas: 4000000, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|             }); | ||||
|             it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketSellSwapQuote, | ||||
|                     { | ||||
|                         extensionContractOpts: { | ||||
|                             feePercentage: 0.05, | ||||
|                             feeRecipient, | ||||
|                         }, | ||||
|                     }, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(contractAddresses.forwarder); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     value: ethAmount, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     gas: 4000000, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|                 const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( | ||||
|                     marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( | ||||
|                     new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), | ||||
|                 ); | ||||
|             }); | ||||
|             it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => { | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( | ||||
|                     marketBuySwapQuote, | ||||
|                     { | ||||
|                         extensionContractOpts: { | ||||
|                             feePercentage: 0.05, | ||||
|                             feeRecipient, | ||||
|                         }, | ||||
|                     }, | ||||
|                 ); | ||||
|                 expect(toAddress).to.deep.equal(contractAddresses.forwarder); | ||||
|                 await web3Wrapper.sendTransactionAsync({ | ||||
|                     from: takerAddress, | ||||
|                     to: toAddress, | ||||
|                     data: calldataHexString, | ||||
|                     value: ethAmount, | ||||
|                     gasPrice: GAS_PRICE, | ||||
|                     gas: 4000000, | ||||
|                 }); | ||||
|                 await expectMakerAndTakerBalancesAsync( | ||||
|                     constants.ZERO_AMOUNT, | ||||
|                     new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), | ||||
|                 ); | ||||
|                 const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( | ||||
|                     marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, | ||||
|                 ); | ||||
|                 const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); | ||||
|                 expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( | ||||
|                     new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), | ||||
|                 ); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|     // tslint:disable-next-line: max-file-line-count | ||||
| }); | ||||
| @@ -22,9 +22,10 @@ import { | ||||
|     BUY_SOURCE_FILTER, | ||||
|     POSITIVE_INF, | ||||
|     SELL_SOURCE_FILTER, | ||||
|     SOURCE_FLAGS, | ||||
|     ZERO_AMOUNT, | ||||
| } from '../src/utils/market_operation_utils/constants'; | ||||
| import { createFillPaths } from '../src/utils/market_operation_utils/fills'; | ||||
| import { createFills } from '../src/utils/market_operation_utils/fills'; | ||||
| import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; | ||||
| import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; | ||||
| import { | ||||
| @@ -49,6 +50,7 @@ const DEFAULT_EXCLUDED = [ | ||||
|     ERC20BridgeSource.Swerve, | ||||
|     ERC20BridgeSource.SushiSwap, | ||||
|     ERC20BridgeSource.MultiHop, | ||||
|     ERC20BridgeSource.Shell, | ||||
| ]; | ||||
| const BUY_SOURCES = BUY_SOURCE_FILTER.sources; | ||||
| const SELL_SOURCES = SELL_SOURCE_FILTER.sources; | ||||
| @@ -107,6 +109,8 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 return ERC20BridgeSource.Mooniswap; | ||||
|             case contractAddresses.sushiswapBridge.toLowerCase(): | ||||
|                 return ERC20BridgeSource.SushiSwap; | ||||
|             case contractAddresses.shellBridge.toLowerCase(): | ||||
|                 return ERC20BridgeSource.Shell; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
| @@ -261,27 +265,6 @@ describe('MarketOperationUtils tests', () => { | ||||
|         return rates; | ||||
|     } | ||||
|  | ||||
|     function getSortedOrderSources(side: MarketOperation, orders: OptimizedMarketOrder[]): ERC20BridgeSource[][] { | ||||
|         return ( | ||||
|             orders | ||||
|                 // Sort orders by descending rate. | ||||
|                 .sort((a, b) => | ||||
|                     b.makerAssetAmount.div(b.takerAssetAmount).comparedTo(a.makerAssetAmount.div(a.takerAssetAmount)), | ||||
|                 ) | ||||
|                 // Then sort fills by descending rate. | ||||
|                 .map(o => { | ||||
|                     return o.fills | ||||
|                         .slice() | ||||
|                         .sort((a, b) => | ||||
|                             side === MarketOperation.Sell | ||||
|                                 ? b.output.div(b.input).comparedTo(a.output.div(a.input)) | ||||
|                                 : b.input.div(b.output).comparedTo(a.input.div(a.output)), | ||||
|                         ) | ||||
|                         .map(f => f.source); | ||||
|                 }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     const NUM_SAMPLES = 3; | ||||
|  | ||||
|     interface RatesBySource { | ||||
| @@ -304,6 +287,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|         [ERC20BridgeSource.Swerve]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.SushiSwap]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.MultiHop]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.Shell]: _.times(NUM_SAMPLES, () => 0), | ||||
|     }; | ||||
|  | ||||
|     const DEFAULT_RATES: RatesBySource = { | ||||
| @@ -349,6 +333,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|         [ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() }, | ||||
|         [ERC20BridgeSource.Native]: { order: createOrder() }, | ||||
|         [ERC20BridgeSource.MultiHop]: {}, | ||||
|         [ERC20BridgeSource.Shell]: {}, | ||||
|     }; | ||||
|  | ||||
|     const DEFAULT_OPS = { | ||||
| @@ -466,7 +451,6 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 maxFallbackSlippage: 100, | ||||
|                 excludedSources: DEFAULT_EXCLUDED, | ||||
|                 allowFallback: false, | ||||
|                 shouldBatchBridgeOrders: false, | ||||
|             }; | ||||
|  | ||||
|             beforeEach(() => { | ||||
| @@ -881,7 +865,6 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor), | ||||
|                         numSamples: 4, | ||||
|                         bridgeSlippage: 0, | ||||
|                         shouldBatchBridgeOrders: false, | ||||
|                     }, | ||||
|                 ); | ||||
|                 const result = ordersAndReport.optimizedOrders; | ||||
| @@ -899,36 +882,48 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); | ||||
|             }); | ||||
|  | ||||
|             it('batches contiguous bridge sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; | ||||
|             it('factors in exchange proxy gas overhead', async () => { | ||||
|                 // Uniswap has a slightly better rate than LiquidityProvider, | ||||
|                 // but LiquidityProvider is better accounting for the EP gas overhead. | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Native]: [0.01, 0.01, 0.01, 0.01], | ||||
|                     [ERC20BridgeSource.Uniswap]: [1, 1, 1, 1], | ||||
|                     [ERC20BridgeSource.LiquidityProvider]: [0.9999, 0.9999, 0.9999, 0.9999], | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                 const optimizer = new MarketOperationUtils( | ||||
|                     MOCK_SAMPLER, | ||||
|                     contractAddresses, | ||||
|                     ORDER_DOMAIN, | ||||
|                     randomAddress(), // liquidity provider registry | ||||
|                 ); | ||||
|                 const gasPrice = 100e9; // 100 gwei | ||||
|                 const exchangeProxyOverhead = (sourceFlags: number) => | ||||
|                     sourceFlags === SOURCE_FLAGS.LiquidityProvider | ||||
|                         ? new BigNumber(3e4).times(gasPrice) | ||||
|                         : new BigNumber(1.3e5).times(gasPrice); | ||||
|                 const improvedOrdersResponse = await optimizer.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
|                     FILL_AMOUNT, | ||||
|                     { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         numSamples: 4, | ||||
|                         excludedSources: [ | ||||
|                             ...DEFAULT_OPTS.excludedSources, | ||||
|                             ERC20BridgeSource.Eth2Dai, | ||||
|                             ERC20BridgeSource.Kyber, | ||||
|                             ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve), | ||||
|                             ERC20BridgeSource.Bancor, | ||||
|                         ], | ||||
|                         shouldBatchBridgeOrders: true, | ||||
|                         exchangeProxyOverhead, | ||||
|                     }, | ||||
|                 ); | ||||
|                 const improvedOrders = improvedOrdersResponse.optimizedOrders; | ||||
|                 expect(improvedOrders).to.be.length(3); | ||||
|                 const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); | ||||
|                 expect(orderFillSources).to.deep.eq([ | ||||
|                     [ERC20BridgeSource.Uniswap], | ||||
|                     [ERC20BridgeSource.Native], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve], | ||||
|                 ]); | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ERC20BridgeSource.LiquidityProvider]; | ||||
|                 expect(orderSources).to.deep.eq(expectedSources); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
| @@ -945,7 +940,6 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 maxFallbackSlippage: 100, | ||||
|                 excludedSources: DEFAULT_EXCLUDED, | ||||
|                 allowFallback: false, | ||||
|                 shouldBatchBridgeOrders: false, | ||||
|             }; | ||||
|  | ||||
|             beforeEach(() => { | ||||
| @@ -1297,35 +1291,52 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); | ||||
|             }); | ||||
|  | ||||
|             it('batches contiguous bridge sources', async () => { | ||||
|                 const rates: RatesBySource = { ...ZERO_RATES }; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; | ||||
|             it('factors in exchange proxy gas overhead', async () => { | ||||
|                 // Uniswap has a slightly better rate than LiquidityProvider, | ||||
|                 // but LiquidityProvider is better accounting for the EP gas overhead. | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Native]: [0.01, 0.01, 0.01, 0.01], | ||||
|                     [ERC20BridgeSource.Uniswap]: [1, 1, 1, 1], | ||||
|                     [ERC20BridgeSource.LiquidityProvider]: [0.9999, 0.9999, 0.9999, 0.9999], | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
|                 const optimizer = new MarketOperationUtils( | ||||
|                     MOCK_SAMPLER, | ||||
|                     contractAddresses, | ||||
|                     ORDER_DOMAIN, | ||||
|                     randomAddress(), // liquidity provider registry | ||||
|                 ); | ||||
|                 const gasPrice = 100e9; // 100 gwei | ||||
|                 const exchangeProxyOverhead = (sourceFlags: number) => | ||||
|                     sourceFlags === SOURCE_FLAGS.LiquidityProvider | ||||
|                         ? new BigNumber(3e4).times(gasPrice) | ||||
|                         : new BigNumber(1.3e5).times(gasPrice); | ||||
|                 const improvedOrdersResponse = await optimizer.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
|                     FILL_AMOUNT, | ||||
|                     { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         numSamples: 4, | ||||
|                         shouldBatchBridgeOrders: true, | ||||
|                         excludedSources: [ | ||||
|                             ...DEFAULT_OPTS.excludedSources, | ||||
|                             ERC20BridgeSource.Eth2Dai, | ||||
|                             ERC20BridgeSource.Kyber, | ||||
|                         ], | ||||
|                         exchangeProxyOverhead, | ||||
|                     }, | ||||
|                 ); | ||||
|                 const improvedOrders = improvedOrdersResponse.optimizedOrders; | ||||
|                 expect(improvedOrders).to.be.length(2); | ||||
|                 const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); | ||||
|                 expect(orderFillSources).to.deep.eq([ | ||||
|                     [ERC20BridgeSource.Native], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], | ||||
|                 ]); | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ERC20BridgeSource.LiquidityProvider]; | ||||
|                 expect(orderSources).to.deep.eq(expectedSources); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('createFillPaths', () => { | ||||
|     describe('createFills', () => { | ||||
|         const takerAssetAmount = new BigNumber(5000000); | ||||
|         const ethToOutputRate = new BigNumber(0.5); | ||||
|         // tslint:disable-next-line:no-object-literal-type-assertion | ||||
| @@ -1359,7 +1370,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|         }; | ||||
|  | ||||
|         it('penalizes native fill based on target amount when target is smaller', () => { | ||||
|             const path = createFillPaths({ | ||||
|             const path = createFills({ | ||||
|                 side: MarketOperation.Sell, | ||||
|                 orders, | ||||
|                 dexQuotes: [], | ||||
| @@ -1372,7 +1383,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|         }); | ||||
|  | ||||
|         it('penalizes native fill based on available amount when target is larger', () => { | ||||
|             const path = createFillPaths({ | ||||
|             const path = createFills({ | ||||
|                 side: MarketOperation.Sell, | ||||
|                 orders, | ||||
|                 dexQuotes: [], | ||||
|   | ||||
| @@ -19,6 +19,7 @@ export * from '../test/generated-wrappers/i_liquidity_provider_registry'; | ||||
| export * from '../test/generated-wrappers/i_m_stable'; | ||||
| export * from '../test/generated-wrappers/i_mooniswap'; | ||||
| export * from '../test/generated-wrappers/i_multi_bridge'; | ||||
| export * from '../test/generated-wrappers/i_shell'; | ||||
| export * from '../test/generated-wrappers/i_uniswap_exchange_quotes'; | ||||
| export * from '../test/generated-wrappers/i_uniswap_v2_router01'; | ||||
| export * from '../test/generated-wrappers/kyber_sampler'; | ||||
| @@ -28,6 +29,7 @@ export * from '../test/generated-wrappers/mooniswap_sampler'; | ||||
| export * from '../test/generated-wrappers/multi_bridge_sampler'; | ||||
| export * from '../test/generated-wrappers/native_order_sampler'; | ||||
| export * from '../test/generated-wrappers/sampler_utils'; | ||||
| export * from '../test/generated-wrappers/shell_sampler'; | ||||
| export * from '../test/generated-wrappers/sushi_swap_sampler'; | ||||
| export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; | ||||
| export * from '../test/generated-wrappers/test_native_order_sampler'; | ||||
|   | ||||
| @@ -24,6 +24,7 @@ | ||||
|         "test/generated-artifacts/IMStable.json", | ||||
|         "test/generated-artifacts/IMooniswap.json", | ||||
|         "test/generated-artifacts/IMultiBridge.json", | ||||
|         "test/generated-artifacts/IShell.json", | ||||
|         "test/generated-artifacts/IUniswapExchangeQuotes.json", | ||||
|         "test/generated-artifacts/IUniswapV2Router01.json", | ||||
|         "test/generated-artifacts/KyberSampler.json", | ||||
| @@ -33,6 +34,7 @@ | ||||
|         "test/generated-artifacts/MultiBridgeSampler.json", | ||||
|         "test/generated-artifacts/NativeOrderSampler.json", | ||||
|         "test/generated-artifacts/SamplerUtils.json", | ||||
|         "test/generated-artifacts/ShellSampler.json", | ||||
|         "test/generated-artifacts/SushiSwapSampler.json", | ||||
|         "test/generated-artifacts/TestERC20BridgeSampler.json", | ||||
|         "test/generated-artifacts/TestNativeOrderSampler.json", | ||||
|   | ||||
| @@ -49,6 +49,10 @@ | ||||
|             { | ||||
|                 "note": "Deploy `BancorBridge` on Mainnet", | ||||
|                 "pr": 2699 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Deploy `ShellBridge` on Mainnet", | ||||
|                 "pr": 2722 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -43,6 +43,7 @@ | ||||
|         "mStableBridge": "0x2bf04fcea05f0989a14d9afa37aa376baca6b2b3", | ||||
|         "mooniswapBridge": "0x02b7eca484ad960fca3f7709e0b2ac81eec3069c", | ||||
|         "sushiswapBridge": "0x47ed0262a0b688dcb836d254c6a2e96b6c48a9f5", | ||||
|         "shellBridge": "0x21fb3862eed7911e0f8219a077247b849846728d", | ||||
|         "transformers": { | ||||
|             "wethTransformer": "0x68c0bb685099dc7cb5c5ce2b26185945b357383e", | ||||
|             "payTakerTransformer": "0x49b9df2c58491764cf40cb052dd4243df63622c7", | ||||
| @@ -94,6 +95,7 @@ | ||||
|         "mStableBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "mooniswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "sushiswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "shellBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "transformers": { | ||||
|             "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", | ||||
|             "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", | ||||
| @@ -145,6 +147,7 @@ | ||||
|         "mStableBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "mooniswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "sushiswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "shellBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "transformers": { | ||||
|             "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", | ||||
|             "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", | ||||
| @@ -196,6 +199,7 @@ | ||||
|         "mStableBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "mooniswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "sushiswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "shellBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "transformers": { | ||||
|             "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", | ||||
|             "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", | ||||
| @@ -247,6 +251,7 @@ | ||||
|         "mStableBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "mooniswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "sushiswapBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "shellBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "transformers": { | ||||
|             "wethTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5", | ||||
|             "payTakerTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", | ||||
|   | ||||
| @@ -44,6 +44,7 @@ export interface ContractAddresses { | ||||
|     mStableBridge: string; | ||||
|     mooniswapBridge: string; | ||||
|     sushiswapBridge: string; | ||||
|     shellBridge: string; | ||||
|     transformers: { | ||||
|         wethTransformer: string; | ||||
|         payTakerTransformer: string; | ||||
|   | ||||
| @@ -17,6 +17,10 @@ | ||||
|             { | ||||
|                 "note": "Regenerate artifacts", | ||||
|                 "pr": 2703 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Update IZeroEx artifact for LiquidityProviderFeature", | ||||
|                 "pr": 2691 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										60
									
								
								packages/contract-artifacts/artifacts/IZeroEx.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										60
									
								
								packages/contract-artifacts/artifacts/IZeroEx.json
									
									
									
										generated
									
									
									
								
							| @@ -3,6 +3,16 @@ | ||||
|     "contractName": "IZeroEx", | ||||
|     "compilerOutput": { | ||||
|         "abi": [ | ||||
|             { | ||||
|                 "anonymous": false, | ||||
|                 "inputs": [ | ||||
|                     { "indexed": true, "internalType": "address", "name": "xAsset", "type": "address" }, | ||||
|                     { "indexed": true, "internalType": "address", "name": "yAsset", "type": "address" }, | ||||
|                     { "indexed": false, "internalType": "address", "name": "providerAddress", "type": "address" } | ||||
|                 ], | ||||
|                 "name": "LiquidityProviderForMarketUpdated", | ||||
|                 "type": "event" | ||||
|             }, | ||||
|             { | ||||
|                 "anonymous": false, | ||||
|                 "inputs": [ | ||||
| @@ -222,6 +232,16 @@ | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [ | ||||
|                     { "internalType": "address", "name": "xAsset", "type": "address" }, | ||||
|                     { "internalType": "address", "name": "yAsset", "type": "address" } | ||||
|                 ], | ||||
|                 "name": "getLiquidityProviderForMarket", | ||||
|                 "outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }], | ||||
|                 "stateMutability": "view", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [ | ||||
|                     { | ||||
| @@ -366,6 +386,19 @@ | ||||
|                 "stateMutability": "nonpayable", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [ | ||||
|                     { "internalType": "address", "name": "makerToken", "type": "address" }, | ||||
|                     { "internalType": "address", "name": "takerToken", "type": "address" }, | ||||
|                     { "internalType": "address payable", "name": "recipient", "type": "address" }, | ||||
|                     { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, | ||||
|                     { "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" } | ||||
|                 ], | ||||
|                 "name": "sellToLiquidityProvider", | ||||
|                 "outputs": [{ "internalType": "uint256", "name": "boughtAmount", "type": "uint256" }], | ||||
|                 "stateMutability": "payable", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [ | ||||
|                     { "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" }, | ||||
| @@ -378,6 +411,17 @@ | ||||
|                 "stateMutability": "payable", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [ | ||||
|                     { "internalType": "address", "name": "xAsset", "type": "address" }, | ||||
|                     { "internalType": "address", "name": "yAsset", "type": "address" }, | ||||
|                     { "internalType": "address", "name": "providerAddress", "type": "address" } | ||||
|                 ], | ||||
|                 "name": "setLiquidityProviderForMarket", | ||||
|                 "outputs": [], | ||||
|                 "stateMutability": "nonpayable", | ||||
|                 "type": "function" | ||||
|             }, | ||||
|             { | ||||
|                 "inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }], | ||||
|                 "name": "setQuoteSigner", | ||||
| @@ -492,6 +536,14 @@ | ||||
|                     "params": { "selector": "The function selector." }, | ||||
|                     "returns": { "impl": "The implementation contract address." } | ||||
|                 }, | ||||
|                 "getLiquidityProviderForMarket(address,address)": { | ||||
|                     "details": "Returns the address of the liquidity provider for a market given     (xAsset, yAsset), or reverts if pool does not exist.", | ||||
|                     "params": { | ||||
|                         "xAsset": "First asset managed by the liquidity provider.", | ||||
|                         "yAsset": "Second asset managed by the liquidity provider." | ||||
|                     }, | ||||
|                     "returns": { "providerAddress": "Address of the liquidity provider." } | ||||
|                 }, | ||||
|                 "getMetaTransactionExecutedBlock((address,address,uint256,uint256,uint256,uint256,bytes,uint256,address,uint256))": { | ||||
|                     "details": "Get the block at which a meta-transaction has been executed.", | ||||
|                     "params": { "mtx": "The meta-transaction." }, | ||||
| @@ -574,6 +626,14 @@ | ||||
|                     }, | ||||
|                     "returns": { "buyAmount": "Amount of `tokens[-1]` bought." } | ||||
|                 }, | ||||
|                 "setLiquidityProviderForMarket(address,address,address)": { | ||||
|                     "details": "Sets address of the liquidity provider for a market given      (xAsset, yAsset).", | ||||
|                     "params": { | ||||
|                         "providerAddress": "Address of the liquidity provider.", | ||||
|                         "xAsset": "First asset managed by the liquidity provider.", | ||||
|                         "yAsset": "Second asset managed by the liquidity provider." | ||||
|                     } | ||||
|                 }, | ||||
|                 "setQuoteSigner(address)": { | ||||
|                     "details": "Replace the optional signer for `transformERC20()` calldata.      Only callable by the owner.", | ||||
|                     "params": { "quoteSigner": "The address of the new calldata signer." } | ||||
|   | ||||
| @@ -17,6 +17,10 @@ | ||||
|             { | ||||
|                 "note": "Regenerate wrappers", | ||||
|                 "pr": 2703 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Update IZeroEx wrapper for LiquidityProviderFeature", | ||||
|                 "pr": 2691 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -36,6 +36,7 @@ import * as ethers from 'ethers'; | ||||
| // tslint:enable:no-unused-variable | ||||
|  | ||||
| export type IZeroExEventArgs = | ||||
|     | IZeroExLiquidityProviderForMarketUpdatedEventArgs | ||||
|     | IZeroExMetaTransactionExecutedEventArgs | ||||
|     | IZeroExMigratedEventArgs | ||||
|     | IZeroExOwnershipTransferredEventArgs | ||||
| @@ -45,6 +46,7 @@ export type IZeroExEventArgs = | ||||
|     | IZeroExTransformerDeployerUpdatedEventArgs; | ||||
|  | ||||
| export enum IZeroExEvents { | ||||
|     LiquidityProviderForMarketUpdated = 'LiquidityProviderForMarketUpdated', | ||||
|     MetaTransactionExecuted = 'MetaTransactionExecuted', | ||||
|     Migrated = 'Migrated', | ||||
|     OwnershipTransferred = 'OwnershipTransferred', | ||||
| @@ -54,6 +56,12 @@ export enum IZeroExEvents { | ||||
|     TransformerDeployerUpdated = 'TransformerDeployerUpdated', | ||||
| } | ||||
|  | ||||
| export interface IZeroExLiquidityProviderForMarketUpdatedEventArgs extends DecodedLogArgs { | ||||
|     xAsset: string; | ||||
|     yAsset: string; | ||||
|     providerAddress: string; | ||||
| } | ||||
|  | ||||
| export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs { | ||||
|     hash: string; | ||||
|     selector: string; | ||||
| @@ -211,6 +219,29 @@ export class IZeroExContract extends BaseContract { | ||||
|      */ | ||||
|     public static ABI(): ContractAbi { | ||||
|         const abi = [ | ||||
|             { | ||||
|                 anonymous: false, | ||||
|                 inputs: [ | ||||
|                     { | ||||
|                         name: 'xAsset', | ||||
|                         type: 'address', | ||||
|                         indexed: true, | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'yAsset', | ||||
|                         type: 'address', | ||||
|                         indexed: true, | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'providerAddress', | ||||
|                         type: 'address', | ||||
|                         indexed: false, | ||||
|                     }, | ||||
|                 ], | ||||
|                 name: 'LiquidityProviderForMarketUpdated', | ||||
|                 outputs: [], | ||||
|                 type: 'event', | ||||
|             }, | ||||
|             { | ||||
|                 anonymous: false, | ||||
|                 inputs: [ | ||||
| @@ -697,6 +728,27 @@ export class IZeroExContract extends BaseContract { | ||||
|                 stateMutability: 'view', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
|                         name: 'xAsset', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'yAsset', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                 ], | ||||
|                 name: 'getLiquidityProviderForMarket', | ||||
|                 outputs: [ | ||||
|                     { | ||||
|                         name: 'providerAddress', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                 ], | ||||
|                 stateMutability: 'view', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
| @@ -1000,6 +1052,39 @@ export class IZeroExContract extends BaseContract { | ||||
|                 stateMutability: 'nonpayable', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
|                         name: 'makerToken', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'takerToken', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'recipient', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'sellAmount', | ||||
|                         type: 'uint256', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'minBuyAmount', | ||||
|                         type: 'uint256', | ||||
|                     }, | ||||
|                 ], | ||||
|                 name: 'sellToLiquidityProvider', | ||||
|                 outputs: [ | ||||
|                     { | ||||
|                         name: 'boughtAmount', | ||||
|                         type: 'uint256', | ||||
|                     }, | ||||
|                 ], | ||||
|                 stateMutability: 'payable', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
| @@ -1029,6 +1114,26 @@ export class IZeroExContract extends BaseContract { | ||||
|                 stateMutability: 'payable', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
|                         name: 'xAsset', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'yAsset', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                     { | ||||
|                         name: 'providerAddress', | ||||
|                         type: 'address', | ||||
|                     }, | ||||
|                 ], | ||||
|                 name: 'setLiquidityProviderForMarket', | ||||
|                 outputs: [], | ||||
|                 stateMutability: 'nonpayable', | ||||
|                 type: 'function', | ||||
|             }, | ||||
|             { | ||||
|                 inputs: [ | ||||
|                     { | ||||
| @@ -1743,6 +1848,60 @@ export class IZeroExContract extends BaseContract { | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * Returns the address of the liquidity provider for a market given | ||||
|      * (xAsset, yAsset), or reverts if pool does not exist. | ||||
|      * @param xAsset First asset managed by the liquidity provider. | ||||
|      * @param yAsset Second asset managed by the liquidity provider. | ||||
|      */ | ||||
|     public getLiquidityProviderForMarket(xAsset: string, yAsset: string): ContractTxFunctionObj<string> { | ||||
|         const self = (this as any) as IZeroExContract; | ||||
|         assert.isString('xAsset', xAsset); | ||||
|         assert.isString('yAsset', yAsset); | ||||
|         const functionSignature = 'getLiquidityProviderForMarket(address,address)'; | ||||
|  | ||||
|         return { | ||||
|             async sendTransactionAsync( | ||||
|                 txData?: Partial<TxData> | undefined, | ||||
|                 opts: SendTransactionOpts = { shouldValidate: true }, | ||||
|             ): Promise<string> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...txData }, | ||||
|                     this.estimateGasAsync.bind(this), | ||||
|                 ); | ||||
|                 if (opts.shouldValidate !== false) { | ||||
|                     await this.callAsync(txDataWithDefaults); | ||||
|                 } | ||||
|                 return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             awaitTransactionSuccessAsync( | ||||
|                 txData?: Partial<TxData>, | ||||
|                 opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, | ||||
|             ): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> { | ||||
|                 return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); | ||||
|             }, | ||||
|             async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ | ||||
|                     data: this.getABIEncodedTransactionData(), | ||||
|                     ...txData, | ||||
|                 }); | ||||
|                 return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             async callAsync(callData: Partial<CallData> = {}, defaultBlock?: BlockParam): Promise<string> { | ||||
|                 BaseContract._assertCallParams(callData, defaultBlock); | ||||
|                 const rawCallResult = await self._performCallAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...callData }, | ||||
|                     defaultBlock, | ||||
|                 ); | ||||
|                 const abiEncoder = self._lookupAbiEncoder(functionSignature); | ||||
|                 BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); | ||||
|                 return abiEncoder.strictDecodeReturnValue<string>(rawCallResult); | ||||
|             }, | ||||
|             getABIEncodedTransactionData(): string { | ||||
|                 return self._strictEncodeArguments(functionSignature, [xAsset.toLowerCase(), yAsset.toLowerCase()]); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * Get the block at which a meta-transaction has been executed. | ||||
|      * @param mtx The meta-transaction. | ||||
| @@ -2447,6 +2606,69 @@ export class IZeroExContract extends BaseContract { | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     public sellToLiquidityProvider( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         recipient: string, | ||||
|         sellAmount: BigNumber, | ||||
|         minBuyAmount: BigNumber, | ||||
|     ): ContractTxFunctionObj<BigNumber> { | ||||
|         const self = (this as any) as IZeroExContract; | ||||
|         assert.isString('makerToken', makerToken); | ||||
|         assert.isString('takerToken', takerToken); | ||||
|         assert.isString('recipient', recipient); | ||||
|         assert.isBigNumber('sellAmount', sellAmount); | ||||
|         assert.isBigNumber('minBuyAmount', minBuyAmount); | ||||
|         const functionSignature = 'sellToLiquidityProvider(address,address,address,uint256,uint256)'; | ||||
|  | ||||
|         return { | ||||
|             async sendTransactionAsync( | ||||
|                 txData?: Partial<TxData> | undefined, | ||||
|                 opts: SendTransactionOpts = { shouldValidate: true }, | ||||
|             ): Promise<string> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...txData }, | ||||
|                     this.estimateGasAsync.bind(this), | ||||
|                 ); | ||||
|                 if (opts.shouldValidate !== false) { | ||||
|                     await this.callAsync(txDataWithDefaults); | ||||
|                 } | ||||
|                 return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             awaitTransactionSuccessAsync( | ||||
|                 txData?: Partial<TxData>, | ||||
|                 opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, | ||||
|             ): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> { | ||||
|                 return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); | ||||
|             }, | ||||
|             async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ | ||||
|                     data: this.getABIEncodedTransactionData(), | ||||
|                     ...txData, | ||||
|                 }); | ||||
|                 return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             async callAsync(callData: Partial<CallData> = {}, defaultBlock?: BlockParam): Promise<BigNumber> { | ||||
|                 BaseContract._assertCallParams(callData, defaultBlock); | ||||
|                 const rawCallResult = await self._performCallAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...callData }, | ||||
|                     defaultBlock, | ||||
|                 ); | ||||
|                 const abiEncoder = self._lookupAbiEncoder(functionSignature); | ||||
|                 BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); | ||||
|                 return abiEncoder.strictDecodeReturnValue<BigNumber>(rawCallResult); | ||||
|             }, | ||||
|             getABIEncodedTransactionData(): string { | ||||
|                 return self._strictEncodeArguments(functionSignature, [ | ||||
|                     makerToken.toLowerCase(), | ||||
|                     takerToken.toLowerCase(), | ||||
|                     recipient.toLowerCase(), | ||||
|                     sellAmount, | ||||
|                     minBuyAmount, | ||||
|                 ]); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * Efficiently sell directly to uniswap/sushiswap. | ||||
|      * @param tokens Sell path. | ||||
| @@ -2509,6 +2731,70 @@ export class IZeroExContract extends BaseContract { | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * Sets address of the liquidity provider for a market given | ||||
|      * (xAsset, yAsset). | ||||
|      * @param xAsset First asset managed by the liquidity provider. | ||||
|      * @param yAsset Second asset managed by the liquidity provider. | ||||
|      * @param providerAddress Address of the liquidity provider. | ||||
|      */ | ||||
|     public setLiquidityProviderForMarket( | ||||
|         xAsset: string, | ||||
|         yAsset: string, | ||||
|         providerAddress: string, | ||||
|     ): ContractTxFunctionObj<void> { | ||||
|         const self = (this as any) as IZeroExContract; | ||||
|         assert.isString('xAsset', xAsset); | ||||
|         assert.isString('yAsset', yAsset); | ||||
|         assert.isString('providerAddress', providerAddress); | ||||
|         const functionSignature = 'setLiquidityProviderForMarket(address,address,address)'; | ||||
|  | ||||
|         return { | ||||
|             async sendTransactionAsync( | ||||
|                 txData?: Partial<TxData> | undefined, | ||||
|                 opts: SendTransactionOpts = { shouldValidate: true }, | ||||
|             ): Promise<string> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...txData }, | ||||
|                     this.estimateGasAsync.bind(this), | ||||
|                 ); | ||||
|                 if (opts.shouldValidate !== false) { | ||||
|                     await this.callAsync(txDataWithDefaults); | ||||
|                 } | ||||
|                 return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             awaitTransactionSuccessAsync( | ||||
|                 txData?: Partial<TxData>, | ||||
|                 opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, | ||||
|             ): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> { | ||||
|                 return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); | ||||
|             }, | ||||
|             async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> { | ||||
|                 const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ | ||||
|                     data: this.getABIEncodedTransactionData(), | ||||
|                     ...txData, | ||||
|                 }); | ||||
|                 return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); | ||||
|             }, | ||||
|             async callAsync(callData: Partial<CallData> = {}, defaultBlock?: BlockParam): Promise<void> { | ||||
|                 BaseContract._assertCallParams(callData, defaultBlock); | ||||
|                 const rawCallResult = await self._performCallAsync( | ||||
|                     { data: this.getABIEncodedTransactionData(), ...callData }, | ||||
|                     defaultBlock, | ||||
|                 ); | ||||
|                 const abiEncoder = self._lookupAbiEncoder(functionSignature); | ||||
|                 BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); | ||||
|                 return abiEncoder.strictDecodeReturnValue<void>(rawCallResult); | ||||
|             }, | ||||
|             getABIEncodedTransactionData(): string { | ||||
|                 return self._strictEncodeArguments(functionSignature, [ | ||||
|                     xAsset.toLowerCase(), | ||||
|                     yAsset.toLowerCase(), | ||||
|                     providerAddress.toLowerCase(), | ||||
|                 ]); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * Replace the optional signer for `transformERC20()` calldata. | ||||
|      * Only callable by the owner. | ||||
|   | ||||
| @@ -125,6 +125,7 @@ export { | ||||
|     IZeroExContract, | ||||
|     IZeroExEventArgs, | ||||
|     IZeroExEvents, | ||||
|     IZeroExLiquidityProviderForMarketUpdatedEventArgs, | ||||
|     IZeroExMetaTransactionExecutedEventArgs, | ||||
|     IZeroExMigratedEventArgs, | ||||
|     IZeroExOwnershipTransferredEventArgs, | ||||
|   | ||||
| @@ -324,6 +324,8 @@ export async function runMigrationsAsync( | ||||
|             uniswapV2Router: NULL_ADDRESS, | ||||
|             uniswapExchangeFactory: NULL_ADDRESS, | ||||
|             mStable: NULL_ADDRESS, | ||||
|             shellBridge: NULL_ADDRESS, | ||||
|             shell: NULL_ADDRESS, | ||||
|             weth: etherToken.address, | ||||
|         }, | ||||
|     ); | ||||
| @@ -401,6 +403,7 @@ export async function runMigrationsAsync( | ||||
|         mStableBridge: NULL_ADDRESS, | ||||
|         mooniswapBridge: NULL_ADDRESS, | ||||
|         sushiswapBridge: NULL_ADDRESS, | ||||
|         shellBridge: NULL_ADDRESS, | ||||
|         exchangeProxy: exchangeProxy.address, | ||||
|         exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, | ||||
|         exchangeProxyTransformerDeployer: txDefaults.from, | ||||
|   | ||||
| @@ -9,6 +9,14 @@ | ||||
|             { | ||||
|                 "note": "Add EP flavor of `IllegalReentrancyError`.", | ||||
|                 "pr": 2657 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added LiquidityProviderFeature errors", | ||||
|                 "pr": 2691 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Added abi encoder support for uint80 lol", | ||||
|                 "pr": 2728 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import * as EncoderMath from '../utils/math'; | ||||
|  | ||||
| export class UIntDataType extends AbstractBlobDataType { | ||||
|     private static readonly _MATCHER = RegExp( | ||||
|         '^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', | ||||
|         '^uint(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', | ||||
|     ); | ||||
|     private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; | ||||
|     private static readonly _MAX_WIDTH: number = 256; | ||||
|   | ||||
| @@ -54,4 +54,5 @@ export const ZeroExRevertErrors = { | ||||
|     Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'), | ||||
|     MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'), | ||||
|     SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'), | ||||
|     LiquidityProvider: require('./revert_errors/zero-ex/liquidity_provider_revert_errors'), | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,47 @@ | ||||
| import { RevertError } from '../../revert_error'; | ||||
| import { Numberish } from '../../types'; | ||||
|  | ||||
| // tslint:disable:max-classes-per-file | ||||
| export class LiquidityProviderIncompleteSellError extends RevertError { | ||||
|     constructor( | ||||
|         providerAddress?: string, | ||||
|         makerToken?: string, | ||||
|         takerToken?: string, | ||||
|         sellAmount?: Numberish, | ||||
|         boughtAmount?: Numberish, | ||||
|         minBuyAmount?: Numberish, | ||||
|     ) { | ||||
|         super( | ||||
|             'LiquidityProviderIncompleteSellError', | ||||
|             'LiquidityProviderIncompleteSellError(address providerAddress, address makerToken, address takerToken, uint256 sellAmount, uint256 boughtAmount, uint256 minBuyAmount)', | ||||
|             { | ||||
|                 providerAddress, | ||||
|                 makerToken, | ||||
|                 takerToken, | ||||
|                 sellAmount, | ||||
|                 boughtAmount, | ||||
|                 minBuyAmount, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export class NoLiquidityProviderForMarketError extends RevertError { | ||||
|     constructor(xAsset?: string, yAsset?: string) { | ||||
|         super( | ||||
|             'NoLiquidityProviderForMarketError', | ||||
|             'NoLiquidityProviderForMarketError(address xAsset, address yAsset)', | ||||
|             { | ||||
|                 xAsset, | ||||
|                 yAsset, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const types = [LiquidityProviderIncompleteSellError, NoLiquidityProviderForMarketError]; | ||||
|  | ||||
| // Register the types we've defined. | ||||
| for (const type of types) { | ||||
|     RevertError.registerType(type); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user