Merge branch 'development' into feat/add-cream

This commit is contained in:
Alex Kroeger 2020-10-11 20:11:24 -07:00 committed by GitHub
commit c6b9ea5723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2450 additions and 1762 deletions

View File

@ -29,6 +29,10 @@
{ {
"note": "Added `CreamBridge`", "note": "Added `CreamBridge`",
"pr": 2715 "pr": 2715
},
{
"note": "Added `ShellBridge`",
"pr": 2722
} }
] ]
}, },

View 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;
}
}

View 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);
}

View File

@ -38,7 +38,7 @@
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES"
}, },
"config": { "config": {
"abis": "./test/generated-artifacts/@(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." "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
}, },
"repository": { "repository": {

View File

@ -33,6 +33,7 @@ import * as IGasToken from '../generated-artifacts/IGasToken.json';
import * as IKyberNetworkProxy from '../generated-artifacts/IKyberNetworkProxy.json'; import * as IKyberNetworkProxy from '../generated-artifacts/IKyberNetworkProxy.json';
import * as IMooniswap from '../generated-artifacts/IMooniswap.json'; import * as IMooniswap from '../generated-artifacts/IMooniswap.json';
import * as IMStable from '../generated-artifacts/IMStable.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 IUniswapExchange from '../generated-artifacts/IUniswapExchange.json';
import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json'; import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json';
import * as IUniswapV2Router01 from '../generated-artifacts/IUniswapV2Router01.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 MStableBridge from '../generated-artifacts/MStableBridge.json';
import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json';
import * as Ownable from '../generated-artifacts/Ownable.json'; import * as Ownable from '../generated-artifacts/Ownable.json';
import * as ShellBridge from '../generated-artifacts/ShellBridge.json';
import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json';
import * as SushiSwapBridge from '../generated-artifacts/SushiSwapBridge.json'; import * as SushiSwapBridge from '../generated-artifacts/SushiSwapBridge.json';
import * as TestBancorBridge from '../generated-artifacts/TestBancorBridge.json'; import * as TestBancorBridge from '../generated-artifacts/TestBancorBridge.json';
@ -80,6 +82,7 @@ export const artifacts = {
MStableBridge: MStableBridge as ContractArtifact, MStableBridge: MStableBridge as ContractArtifact,
MixinGasToken: MixinGasToken as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact,
MooniswapBridge: MooniswapBridge as ContractArtifact, MooniswapBridge: MooniswapBridge as ContractArtifact,
ShellBridge: ShellBridge as ContractArtifact,
SushiSwapBridge: SushiSwapBridge as ContractArtifact, SushiSwapBridge: SushiSwapBridge as ContractArtifact,
UniswapBridge: UniswapBridge as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact,
UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, UniswapV2Bridge: UniswapV2Bridge as ContractArtifact,
@ -99,6 +102,7 @@ export const artifacts = {
IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact,
IMStable: IMStable as ContractArtifact, IMStable: IMStable as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact, IMooniswap: IMooniswap as ContractArtifact,
IShell: IShell as ContractArtifact,
IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact,
IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,

View File

@ -31,6 +31,7 @@ export * from '../generated-wrappers/i_gas_token';
export * from '../generated-wrappers/i_kyber_network_proxy'; export * from '../generated-wrappers/i_kyber_network_proxy';
export * from '../generated-wrappers/i_m_stable'; export * from '../generated-wrappers/i_m_stable';
export * from '../generated-wrappers/i_mooniswap'; 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';
export * from '../generated-wrappers/i_uniswap_exchange_factory'; export * from '../generated-wrappers/i_uniswap_exchange_factory';
export * from '../generated-wrappers/i_uniswap_v2_router01'; 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/mooniswap_bridge';
export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/multi_asset_proxy';
export * from '../generated-wrappers/ownable'; export * from '../generated-wrappers/ownable';
export * from '../generated-wrappers/shell_bridge';
export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/static_call_proxy';
export * from '../generated-wrappers/sushi_swap_bridge'; export * from '../generated-wrappers/sushi_swap_bridge';
export * from '../generated-wrappers/test_bancor_bridge'; export * from '../generated-wrappers/test_bancor_bridge';

View File

@ -33,6 +33,7 @@ import * as IGasToken from '../test/generated-artifacts/IGasToken.json';
import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json';
import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json';
import * as IMStable from '../test/generated-artifacts/IMStable.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 IUniswapExchange from '../test/generated-artifacts/IUniswapExchange.json';
import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json'; import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json';
import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.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 MStableBridge from '../test/generated-artifacts/MStableBridge.json';
import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.json'; import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.json';
import * as Ownable from '../test/generated-artifacts/Ownable.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 StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json';
import * as SushiSwapBridge from '../test/generated-artifacts/SushiSwapBridge.json'; import * as SushiSwapBridge from '../test/generated-artifacts/SushiSwapBridge.json';
import * as TestBancorBridge from '../test/generated-artifacts/TestBancorBridge.json'; import * as TestBancorBridge from '../test/generated-artifacts/TestBancorBridge.json';
@ -80,6 +82,7 @@ export const artifacts = {
MStableBridge: MStableBridge as ContractArtifact, MStableBridge: MStableBridge as ContractArtifact,
MixinGasToken: MixinGasToken as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact,
MooniswapBridge: MooniswapBridge as ContractArtifact, MooniswapBridge: MooniswapBridge as ContractArtifact,
ShellBridge: ShellBridge as ContractArtifact,
SushiSwapBridge: SushiSwapBridge as ContractArtifact, SushiSwapBridge: SushiSwapBridge as ContractArtifact,
UniswapBridge: UniswapBridge as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact,
UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, UniswapV2Bridge: UniswapV2Bridge as ContractArtifact,
@ -99,6 +102,7 @@ export const artifacts = {
IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact,
IMStable: IMStable as ContractArtifact, IMStable: IMStable as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact, IMooniswap: IMooniswap as ContractArtifact,
IShell: IShell as ContractArtifact,
IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact,
IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,

View File

@ -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_kyber_network_proxy';
export * from '../test/generated-wrappers/i_m_stable'; export * from '../test/generated-wrappers/i_m_stable';
export * from '../test/generated-wrappers/i_mooniswap'; 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';
export * from '../test/generated-wrappers/i_uniswap_exchange_factory'; export * from '../test/generated-wrappers/i_uniswap_exchange_factory';
export * from '../test/generated-wrappers/i_uniswap_v2_router01'; 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/mooniswap_bridge';
export * from '../test/generated-wrappers/multi_asset_proxy'; export * from '../test/generated-wrappers/multi_asset_proxy';
export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/ownable';
export * from '../test/generated-wrappers/shell_bridge';
export * from '../test/generated-wrappers/static_call_proxy'; export * from '../test/generated-wrappers/static_call_proxy';
export * from '../test/generated-wrappers/sushi_swap_bridge'; export * from '../test/generated-wrappers/sushi_swap_bridge';
export * from '../test/generated-wrappers/test_bancor_bridge'; export * from '../test/generated-wrappers/test_bancor_bridge';

View File

@ -31,6 +31,7 @@
"generated-artifacts/IKyberNetworkProxy.json", "generated-artifacts/IKyberNetworkProxy.json",
"generated-artifacts/IMStable.json", "generated-artifacts/IMStable.json",
"generated-artifacts/IMooniswap.json", "generated-artifacts/IMooniswap.json",
"generated-artifacts/IShell.json",
"generated-artifacts/IUniswapExchange.json", "generated-artifacts/IUniswapExchange.json",
"generated-artifacts/IUniswapExchangeFactory.json", "generated-artifacts/IUniswapExchangeFactory.json",
"generated-artifacts/IUniswapV2Router01.json", "generated-artifacts/IUniswapV2Router01.json",
@ -42,6 +43,7 @@
"generated-artifacts/MooniswapBridge.json", "generated-artifacts/MooniswapBridge.json",
"generated-artifacts/MultiAssetProxy.json", "generated-artifacts/MultiAssetProxy.json",
"generated-artifacts/Ownable.json", "generated-artifacts/Ownable.json",
"generated-artifacts/ShellBridge.json",
"generated-artifacts/StaticCallProxy.json", "generated-artifacts/StaticCallProxy.json",
"generated-artifacts/SushiSwapBridge.json", "generated-artifacts/SushiSwapBridge.json",
"generated-artifacts/TestBancorBridge.json", "generated-artifacts/TestBancorBridge.json",
@ -84,6 +86,7 @@
"test/generated-artifacts/IKyberNetworkProxy.json", "test/generated-artifacts/IKyberNetworkProxy.json",
"test/generated-artifacts/IMStable.json", "test/generated-artifacts/IMStable.json",
"test/generated-artifacts/IMooniswap.json", "test/generated-artifacts/IMooniswap.json",
"test/generated-artifacts/IShell.json",
"test/generated-artifacts/IUniswapExchange.json", "test/generated-artifacts/IUniswapExchange.json",
"test/generated-artifacts/IUniswapExchangeFactory.json", "test/generated-artifacts/IUniswapExchangeFactory.json",
"test/generated-artifacts/IUniswapV2Router01.json", "test/generated-artifacts/IUniswapV2Router01.json",
@ -95,6 +98,7 @@
"test/generated-artifacts/MooniswapBridge.json", "test/generated-artifacts/MooniswapBridge.json",
"test/generated-artifacts/MultiAssetProxy.json", "test/generated-artifacts/MultiAssetProxy.json",
"test/generated-artifacts/Ownable.json", "test/generated-artifacts/Ownable.json",
"test/generated-artifacts/ShellBridge.json",
"test/generated-artifacts/StaticCallProxy.json", "test/generated-artifacts/StaticCallProxy.json",
"test/generated-artifacts/SushiSwapBridge.json", "test/generated-artifacts/SushiSwapBridge.json",
"test/generated-artifacts/TestBancorBridge.json", "test/generated-artifacts/TestBancorBridge.json",

View File

@ -56,6 +56,8 @@ contract DeploymentConstants {
address constant private MUSD_ADDRESS = 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5; address constant private MUSD_ADDRESS = 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5;
/// @dev Mainnet address of the Mooniswap Registry contract /// @dev Mainnet address of the Mooniswap Registry contract
address constant private MOONISWAP_REGISTRY = 0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303; address constant private MOONISWAP_REGISTRY = 0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303;
/// @dev Mainnet address of the Shell contract
address constant private SHELL_CONTRACT = 0x2E703D658f8dd21709a7B458967aB4081F8D3d05;
// // Ropsten addresses /////////////////////////////////////////////////////// // // Ropsten addresses ///////////////////////////////////////////////////////
// /// @dev Mainnet address of the WETH contract. // /// @dev Mainnet address of the WETH contract.
@ -296,4 +298,14 @@ contract DeploymentConstants {
{ {
return MOONISWAP_REGISTRY; 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;
}
} }

View File

@ -57,6 +57,14 @@
{ {
"note": "Fix versioning (`_encodeVersion()`) bug", "note": "Fix versioning (`_encodeVersion()`) bug",
"pr": 2703 "pr": 2703
},
{
"note": "Added LiquidityProviderFeature",
"pr": 2691
},
{
"note": "Added `Shell` into FQT",
"pr": 2722
} }
] ]
}, },

View File

@ -26,6 +26,7 @@ import "./features/ISignatureValidatorFeature.sol";
import "./features/ITransformERC20Feature.sol"; import "./features/ITransformERC20Feature.sol";
import "./features/IMetaTransactionsFeature.sol"; import "./features/IMetaTransactionsFeature.sol";
import "./features/IUniswapFeature.sol"; import "./features/IUniswapFeature.sol";
import "./features/ILiquidityProviderFeature.sol";
/// @dev Interface for a fully featured Exchange Proxy. /// @dev Interface for a fully featured Exchange Proxy.
@ -36,7 +37,8 @@ interface IZeroEx is
ISignatureValidatorFeature, ISignatureValidatorFeature,
ITransformERC20Feature, ITransformERC20Feature,
IMetaTransactionsFeature, IMetaTransactionsFeature,
IUniswapFeature IUniswapFeature,
ILiquidityProviderFeature
{ {
// solhint-disable state-visibility // solhint-disable state-visibility

View File

@ -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
);
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -184,7 +184,7 @@ contract MetaTransactionsFeature is
/// @dev Execute a meta-transaction via `sender`. Privileged variant. /// @dev Execute a meta-transaction via `sender`. Privileged variant.
/// Only callable from within. /// 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 mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`. /// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call. /// @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. /// @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) function _callSelf(bytes32 hash, bytes memory callData, uint256 value)
private private
returns (bytes memory returnResult) returns (bytes memory returnResult)

View File

@ -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 }
}
}

View File

@ -36,7 +36,8 @@ library LibStorage {
TokenSpender, TokenSpender,
TransformERC20, TransformERC20,
MetaTransactions, MetaTransactions,
ReentrancyGuard ReentrancyGuard,
LiquidityProvider
} }
/// @dev Get the storage slot given a storage ID. We assign unique, well-spaced /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced

View File

@ -26,6 +26,7 @@ import "./mixins/MixinKyber.sol";
import "./mixins/MixinMooniswap.sol"; import "./mixins/MixinMooniswap.sol";
import "./mixins/MixinMStable.sol"; import "./mixins/MixinMStable.sol";
import "./mixins/MixinOasis.sol"; import "./mixins/MixinOasis.sol";
import "./mixins/MixinShell.sol";
import "./mixins/MixinUniswap.sol"; import "./mixins/MixinUniswap.sol";
import "./mixins/MixinUniswapV2.sol"; import "./mixins/MixinUniswapV2.sol";
import "./mixins/MixinZeroExBridge.sol"; import "./mixins/MixinZeroExBridge.sol";
@ -38,6 +39,7 @@ contract BridgeAdapter is
MixinMooniswap, MixinMooniswap,
MixinMStable, MixinMStable,
MixinOasis, MixinOasis,
MixinShell,
MixinUniswap, MixinUniswap,
MixinUniswapV2, MixinUniswapV2,
MixinZeroExBridge MixinZeroExBridge
@ -49,6 +51,7 @@ contract BridgeAdapter is
address private immutable MOONISWAP_BRIDGE_ADDRESS; address private immutable MOONISWAP_BRIDGE_ADDRESS;
address private immutable MSTABLE_BRIDGE_ADDRESS; address private immutable MSTABLE_BRIDGE_ADDRESS;
address private immutable OASIS_BRIDGE_ADDRESS; address private immutable OASIS_BRIDGE_ADDRESS;
address private immutable SHELL_BRIDGE_ADDRESS;
address private immutable UNISWAP_BRIDGE_ADDRESS; address private immutable UNISWAP_BRIDGE_ADDRESS;
address private immutable UNISWAP_V2_BRIDGE_ADDRESS; address private immutable UNISWAP_V2_BRIDGE_ADDRESS;
@ -76,6 +79,7 @@ contract BridgeAdapter is
MixinMooniswap(addresses) MixinMooniswap(addresses)
MixinMStable(addresses) MixinMStable(addresses)
MixinOasis(addresses) MixinOasis(addresses)
MixinShell(addresses)
MixinUniswap(addresses) MixinUniswap(addresses)
MixinUniswapV2(addresses) MixinUniswapV2(addresses)
MixinZeroExBridge() MixinZeroExBridge()
@ -86,6 +90,7 @@ contract BridgeAdapter is
MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge; MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge;
MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge; MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge;
OASIS_BRIDGE_ADDRESS = addresses.oasisBridge; OASIS_BRIDGE_ADDRESS = addresses.oasisBridge;
SHELL_BRIDGE_ADDRESS = addresses.shellBridge;
UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge; UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge;
UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge; UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge;
} }
@ -159,6 +164,12 @@ contract BridgeAdapter is
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == SHELL_BRIDGE_ADDRESS) {
boughtAmount = _tradeShell(
buyToken,
sellAmount,
bridgeData
);
} else { } else {
boughtAmount = _tradeZeroExBridge( boughtAmount = _tradeZeroExBridge(
bridgeAddress, bridgeAddress,

View File

@ -29,6 +29,7 @@ contract MixinAdapterAddresses
address mooniswapBridge; address mooniswapBridge;
address mStableBridge; address mStableBridge;
address oasisBridge; address oasisBridge;
address shellBridge;
address uniswapBridge; address uniswapBridge;
address uniswapV2Bridge; address uniswapV2Bridge;
// Exchanges // Exchanges
@ -37,6 +38,7 @@ contract MixinAdapterAddresses
address uniswapV2Router; address uniswapV2Router;
address uniswapExchangeFactory; address uniswapExchangeFactory;
address mStable; address mStable;
address shell;
// Other // Other
address weth; address weth;
} }

View File

@ -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;
}
}

View 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;
}
}

View File

@ -39,9 +39,9 @@
"publish:private": "yarn build && gitpkg publish" "publish:private": "yarn build && gitpkg publish"
}, },
"config": { "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: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": { "repository": {
"type": "git", "type": "git",
@ -55,6 +55,7 @@
"devDependencies": { "devDependencies": {
"@0x/abi-gen": "^5.3.1", "@0x/abi-gen": "^5.3.1",
"@0x/contracts-gen": "^2.0.10", "@0x/contracts-gen": "^2.0.10",
"@0x/contracts-erc20": "^3.2.1",
"@0x/contracts-test-utils": "^5.3.4", "@0x/contracts-test-utils": "^5.3.4",
"@0x/dev-utils": "^3.3.0", "@0x/dev-utils": "^3.3.0",
"@0x/order-utils": "^10.3.0", "@0x/order-utils": "^10.3.0",

View File

@ -18,6 +18,7 @@ import * as ISimpleFunctionRegistryFeature from '../generated-artifacts/ISimpleF
import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json'; import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json';
import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json'; import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json';
import * as IZeroEx from '../generated-artifacts/IZeroEx.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 LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json'; import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json';
import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json';
@ -52,4 +53,5 @@ export const artifacts = {
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact, BridgeAdapter: BridgeAdapter as ContractArtifact,
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
}; };

View File

@ -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_transform_erc20_feature';
export * from '../generated-wrappers/i_zero_ex'; export * from '../generated-wrappers/i_zero_ex';
export * from '../generated-wrappers/initial_migration'; export * from '../generated-wrappers/initial_migration';
export * from '../generated-wrappers/liquidity_provider_feature';
export * from '../generated-wrappers/log_metadata_transformer'; export * from '../generated-wrappers/log_metadata_transformer';
export * from '../generated-wrappers/meta_transactions_feature'; export * from '../generated-wrappers/meta_transactions_feature';
export * from '../generated-wrappers/ownable_feature'; export * from '../generated-wrappers/ownable_feature';

View File

@ -24,6 +24,7 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json';
import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json';
import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json';
import * as IGasToken from '../test/generated-artifacts/IGasToken.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 IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json';
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.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 LibBootstrap from '../test/generated-artifacts/LibBootstrap.json';
import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json';
import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.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 LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json';
import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json';
import * as LibMigrate from '../test/generated-artifacts/LibMigrate.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 LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json';
import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json';
import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.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 LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json';
import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.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 MixinMooniswap from '../test/generated-artifacts/MixinMooniswap.json';
import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json'; import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json';
import * as MixinOasis from '../test/generated-artifacts/MixinOasis.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 MixinUniswap from '../test/generated-artifacts/MixinUniswap.json';
import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json'; import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json';
import * as MixinZeroExBridge from '../test/generated-artifacts/MixinZeroExBridge.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 PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json'; import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json';
import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.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 TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json';
import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json'; import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json';
@ -104,6 +110,7 @@ export const artifacts = {
IZeroEx: IZeroEx as ContractArtifact, IZeroEx: IZeroEx as ContractArtifact,
ZeroEx: ZeroEx as ContractArtifact, ZeroEx: ZeroEx as ContractArtifact,
LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact,
LibLiquidityProviderRichErrors: LibLiquidityProviderRichErrors as ContractArtifact,
LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact,
LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact,
LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact,
@ -120,6 +127,7 @@ export const artifacts = {
BootstrapFeature: BootstrapFeature as ContractArtifact, BootstrapFeature: BootstrapFeature as ContractArtifact,
IBootstrapFeature: IBootstrapFeature as ContractArtifact, IBootstrapFeature: IBootstrapFeature as ContractArtifact,
IFeature: IFeature as ContractArtifact, IFeature: IFeature as ContractArtifact,
ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact,
IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact,
IOwnableFeature: IOwnableFeature as ContractArtifact, IOwnableFeature: IOwnableFeature as ContractArtifact,
ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact, ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact,
@ -127,6 +135,7 @@ export const artifacts = {
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature as ContractArtifact, IUniswapFeature: IUniswapFeature as ContractArtifact,
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
OwnableFeature: OwnableFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact,
SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact, SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact,
@ -142,6 +151,7 @@ export const artifacts = {
InitialMigration: InitialMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact,
LibBootstrap: LibBootstrap as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact,
LibMigrate: LibMigrate as ContractArtifact, LibMigrate: LibMigrate as ContractArtifact,
LibLiquidityProviderStorage: LibLiquidityProviderStorage as ContractArtifact,
LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact,
LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact,
LibProxyStorage: LibProxyStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact,
@ -167,6 +177,7 @@ export const artifacts = {
MixinMStable: MixinMStable as ContractArtifact, MixinMStable: MixinMStable as ContractArtifact,
MixinMooniswap: MixinMooniswap as ContractArtifact, MixinMooniswap: MixinMooniswap as ContractArtifact,
MixinOasis: MixinOasis as ContractArtifact, MixinOasis: MixinOasis as ContractArtifact,
MixinShell: MixinShell as ContractArtifact,
MixinUniswap: MixinUniswap as ContractArtifact, MixinUniswap: MixinUniswap as ContractArtifact,
MixinUniswapV2: MixinUniswapV2 as ContractArtifact, MixinUniswapV2: MixinUniswapV2 as ContractArtifact,
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact, MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
@ -174,6 +185,7 @@ export const artifacts = {
IExchange: IExchange as ContractArtifact, IExchange: IExchange as ContractArtifact,
IGasToken: IGasToken as ContractArtifact, IGasToken: IGasToken as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
TestBridge: TestBridge as ContractArtifact,
TestCallTarget: TestCallTarget as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact,
TestDelegateCaller: TestDelegateCaller as ContractArtifact, TestDelegateCaller: TestDelegateCaller as ContractArtifact,
TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact,

View 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,
);
});
});
});

View File

@ -73,6 +73,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
uniswapExchangeFactory: NULL_ADDRESS, uniswapExchangeFactory: NULL_ADDRESS,
mStable: NULL_ADDRESS, mStable: NULL_ADDRESS,
weth: NULL_ADDRESS, weth: NULL_ADDRESS,
shellBridge: NULL_ADDRESS,
shell: NULL_ADDRESS,
}, },
); );
transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync(

View File

@ -22,6 +22,7 @@ export * from '../test/generated-wrappers/i_exchange';
export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_feature';
export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_flash_wallet';
export * from '../test/generated-wrappers/i_gas_token'; 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_meta_transactions_feature';
export * from '../test/generated-wrappers/i_ownable_feature'; export * from '../test/generated-wrappers/i_ownable_feature';
export * from '../test/generated-wrappers/i_signature_validator_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_bootstrap';
export * from '../test/generated-wrappers/lib_common_rich_errors'; export * from '../test/generated-wrappers/lib_common_rich_errors';
export * from '../test/generated-wrappers/lib_erc20_transformer'; 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_rich_errors';
export * from '../test/generated-wrappers/lib_meta_transactions_storage'; export * from '../test/generated-wrappers/lib_meta_transactions_storage';
export * from '../test/generated-wrappers/lib_migrate'; 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_rich_errors';
export * from '../test/generated-wrappers/lib_transform_erc20_storage'; export * from '../test/generated-wrappers/lib_transform_erc20_storage';
export * from '../test/generated-wrappers/lib_wallet_rich_errors'; 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/log_metadata_transformer';
export * from '../test/generated-wrappers/meta_transactions_feature'; export * from '../test/generated-wrappers/meta_transactions_feature';
export * from '../test/generated-wrappers/mixin_adapter_addresses'; 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_m_stable';
export * from '../test/generated-wrappers/mixin_mooniswap'; export * from '../test/generated-wrappers/mixin_mooniswap';
export * from '../test/generated-wrappers/mixin_oasis'; 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';
export * from '../test/generated-wrappers/mixin_uniswap_v2'; export * from '../test/generated-wrappers/mixin_uniswap_v2';
export * from '../test/generated-wrappers/mixin_zero_ex_bridge'; 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/pay_taker_transformer';
export * from '../test/generated-wrappers/signature_validator_feature'; export * from '../test/generated-wrappers/signature_validator_feature';
export * from '../test/generated-wrappers/simple_function_registry_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_call_target';
export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_delegate_caller';
export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge';

View File

@ -16,6 +16,7 @@
"generated-artifacts/ITransformERC20Feature.json", "generated-artifacts/ITransformERC20Feature.json",
"generated-artifacts/IZeroEx.json", "generated-artifacts/IZeroEx.json",
"generated-artifacts/InitialMigration.json", "generated-artifacts/InitialMigration.json",
"generated-artifacts/LiquidityProviderFeature.json",
"generated-artifacts/LogMetadataTransformer.json", "generated-artifacts/LogMetadataTransformer.json",
"generated-artifacts/MetaTransactionsFeature.json", "generated-artifacts/MetaTransactionsFeature.json",
"generated-artifacts/OwnableFeature.json", "generated-artifacts/OwnableFeature.json",
@ -45,6 +46,7 @@
"test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFeature.json",
"test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IFlashWallet.json",
"test/generated-artifacts/IGasToken.json", "test/generated-artifacts/IGasToken.json",
"test/generated-artifacts/ILiquidityProviderFeature.json",
"test/generated-artifacts/IMetaTransactionsFeature.json", "test/generated-artifacts/IMetaTransactionsFeature.json",
"test/generated-artifacts/IOwnableFeature.json", "test/generated-artifacts/IOwnableFeature.json",
"test/generated-artifacts/ISignatureValidatorFeature.json", "test/generated-artifacts/ISignatureValidatorFeature.json",
@ -58,6 +60,8 @@
"test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibBootstrap.json",
"test/generated-artifacts/LibCommonRichErrors.json", "test/generated-artifacts/LibCommonRichErrors.json",
"test/generated-artifacts/LibERC20Transformer.json", "test/generated-artifacts/LibERC20Transformer.json",
"test/generated-artifacts/LibLiquidityProviderRichErrors.json",
"test/generated-artifacts/LibLiquidityProviderStorage.json",
"test/generated-artifacts/LibMetaTransactionsRichErrors.json", "test/generated-artifacts/LibMetaTransactionsRichErrors.json",
"test/generated-artifacts/LibMetaTransactionsStorage.json", "test/generated-artifacts/LibMetaTransactionsStorage.json",
"test/generated-artifacts/LibMigrate.json", "test/generated-artifacts/LibMigrate.json",
@ -76,6 +80,7 @@
"test/generated-artifacts/LibTransformERC20RichErrors.json", "test/generated-artifacts/LibTransformERC20RichErrors.json",
"test/generated-artifacts/LibTransformERC20Storage.json", "test/generated-artifacts/LibTransformERC20Storage.json",
"test/generated-artifacts/LibWalletRichErrors.json", "test/generated-artifacts/LibWalletRichErrors.json",
"test/generated-artifacts/LiquidityProviderFeature.json",
"test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/LogMetadataTransformer.json",
"test/generated-artifacts/MetaTransactionsFeature.json", "test/generated-artifacts/MetaTransactionsFeature.json",
"test/generated-artifacts/MixinAdapterAddresses.json", "test/generated-artifacts/MixinAdapterAddresses.json",
@ -85,6 +90,7 @@
"test/generated-artifacts/MixinMStable.json", "test/generated-artifacts/MixinMStable.json",
"test/generated-artifacts/MixinMooniswap.json", "test/generated-artifacts/MixinMooniswap.json",
"test/generated-artifacts/MixinOasis.json", "test/generated-artifacts/MixinOasis.json",
"test/generated-artifacts/MixinShell.json",
"test/generated-artifacts/MixinUniswap.json", "test/generated-artifacts/MixinUniswap.json",
"test/generated-artifacts/MixinUniswapV2.json", "test/generated-artifacts/MixinUniswapV2.json",
"test/generated-artifacts/MixinZeroExBridge.json", "test/generated-artifacts/MixinZeroExBridge.json",
@ -92,6 +98,7 @@
"test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/PayTakerTransformer.json",
"test/generated-artifacts/SignatureValidatorFeature.json", "test/generated-artifacts/SignatureValidatorFeature.json",
"test/generated-artifacts/SimpleFunctionRegistryFeature.json", "test/generated-artifacts/SimpleFunctionRegistryFeature.json",
"test/generated-artifacts/TestBridge.json",
"test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestCallTarget.json",
"test/generated-artifacts/TestDelegateCaller.json", "test/generated-artifacts/TestDelegateCaller.json",
"test/generated-artifacts/TestFillQuoteTransformerBridge.json", "test/generated-artifacts/TestFillQuoteTransformerBridge.json",

View File

@ -133,6 +133,22 @@
{ {
"note": "Respect max slippage in EP consumer", "note": "Respect max slippage in EP consumer",
"pr": 2712 "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
} }
] ]
}, },

View File

@ -78,16 +78,22 @@ contract ApproximateBuys {
for (uint256 i = 0; i < makerTokenAmounts.length; i++) { for (uint256 i = 0; i < makerTokenAmounts.length; i++) {
for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) { for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) {
// adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER
sellAmount = LibMath.getPartialAmountCeil( sellAmount = _safeGetPartialAmountCeil(
makerTokenAmounts[i], makerTokenAmounts[i],
buyAmount, buyAmount,
sellAmount sellAmount
); );
sellAmount = LibMath.getPartialAmountCeil( if (sellAmount == 0) {
break;
}
sellAmount = _safeGetPartialAmountCeil(
(ONE_HUNDED_PERCENT_BPS + APPROXIMATE_BUY_TARGET_EPSILON_BPS), (ONE_HUNDED_PERCENT_BPS + APPROXIMATE_BUY_TARGET_EPSILON_BPS),
ONE_HUNDED_PERCENT_BPS, ONE_HUNDED_PERCENT_BPS,
sellAmount sellAmount
); );
if (sellAmount == 0) {
break;
}
uint256 _buyAmount = opts.getSellQuoteCallback( uint256 _buyAmount = opts.getSellQuoteCallback(
opts.takerTokenData, opts.takerTokenData,
opts.makerTokenData, 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 // 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 // if we hit a max iteration limit
// We scale the sell amount to get the approximate target // We scale the sell amount to get the approximate target
takerTokenAmounts[i] = LibMath.getPartialAmountCeil( takerTokenAmounts[i] = _safeGetPartialAmountCeil(
makerTokenAmounts[i], makerTokenAmounts[i],
buyAmount, buyAmount,
sellAmount 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;
}
} }

View File

@ -28,6 +28,7 @@ import "./MultiBridgeSampler.sol";
import "./MStableSampler.sol"; import "./MStableSampler.sol";
import "./MooniswapSampler.sol"; import "./MooniswapSampler.sol";
import "./NativeOrderSampler.sol"; import "./NativeOrderSampler.sol";
import "./ShellSampler.sol";
import "./SushiSwapSampler.sol"; import "./SushiSwapSampler.sol";
import "./TwoHopSampler.sol"; import "./TwoHopSampler.sol";
import "./UniswapSampler.sol"; import "./UniswapSampler.sol";
@ -44,6 +45,7 @@ contract ERC20BridgeSampler is
MooniswapSampler, MooniswapSampler,
MultiBridgeSampler, MultiBridgeSampler,
NativeOrderSampler, NativeOrderSampler,
ShellSampler,
SushiSwapSampler, SushiSwapSampler,
TwoHopSampler, TwoHopSampler,
UniswapSampler, UniswapSampler,

View 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;
}
}
}

View 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);
}

View File

@ -17,7 +17,7 @@
"compile": "sol-compiler", "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": "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", "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", "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", "test": "yarn run_mocha",
"rebuild_and_test": "run-s clean build test", "rebuild_and_test": "run-s clean build test",
@ -38,7 +38,7 @@
"config": { "config": {
"publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"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": { "postpublish": {
"assets": [] "assets": []
} }

View File

@ -1,9 +1,10 @@
import { BigNumber } from '@0x/utils'; import { BigNumber, logUtils } from '@0x/utils';
import { import {
ExchangeProxyContractOpts, ExchangeProxyContractOpts,
ExtensionContractType, ExtensionContractType,
ForwarderExtensionContractOpts, ForwarderExtensionContractOpts,
LogFunction,
OrderPrunerOpts, OrderPrunerOpts,
OrderPrunerPermittedFeeTypes, OrderPrunerPermittedFeeTypes,
RfqtRequestOpts, RfqtRequestOpts,
@ -89,6 +90,11 @@ const DEFAULT_RFQT_REQUEST_OPTS: Partial<RfqtRequestOpts> = {
makerEndpointMaxResponseTimeMs: 1000, 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 = { export const constants = {
ETH_GAS_STATION_API_URL, ETH_GAS_STATION_API_URL,
PROTOCOL_FEE_MULTIPLIER, PROTOCOL_FEE_MULTIPLIER,
@ -113,4 +119,6 @@ export const constants = {
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3', BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
DEFAULT_INFO_LOGGER,
DEFAULT_WARNING_LOGGER,
}; };

View File

@ -119,6 +119,7 @@ export {
SwapQuoterRfqtOpts, SwapQuoterRfqtOpts,
} from './types'; } from './types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { SOURCE_FLAGS } from './utils/market_operation_utils/constants';
export { export {
Parameters, Parameters,
SamplerContractCall, SamplerContractCall,
@ -133,10 +134,10 @@ export {
CurveInfo, CurveInfo,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
ExchangeProxyOverhead,
FeeSchedule, FeeSchedule,
Fill, Fill,
FillData, FillData,
FillFlags,
GetMarketOrdersRfqtOpts, GetMarketOrdersRfqtOpts,
KyberFillData, KyberFillData,
LiquidityProviderFillData, LiquidityProviderFillData,

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -18,15 +18,11 @@ import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer'; 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 { export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider; public readonly provider: ZeroExProvider;
public readonly chainId: number; public readonly chainId: number;
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
private readonly _contractAddresses: ContractAddresses; private readonly _contractAddresses: ContractAddresses;
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer; private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
@ -45,8 +41,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
this.provider = provider; this.provider = provider;
this.chainId = chainId; this.chainId = chainId;
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(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( this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
supportedProvider, supportedProvider,
this._contractAddresses, this._contractAddresses,
@ -100,13 +94,12 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
} }
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> { private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
// ( akroeger)leaving this switch to use different contracts in the future
switch (opts.useExtensionContract) { switch (opts.useExtensionContract) {
case ExtensionContractType.Forwarder:
return this._forwarderConsumer;
case ExtensionContractType.ExchangeProxy: case ExtensionContractType.ExchangeProxy:
return this._exchangeProxyConsumer; return this._exchangeProxyConsumer;
default: default:
return this._exchangeConsumer; return this._exchangeProxyConsumer;
} }
} }
} }

View File

@ -9,7 +9,6 @@ import {
TokenAdjacencyGraph, TokenAdjacencyGraph,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
import { QuoteReport } from './utils/quote_report_generator'; 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). * 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]>; [endpoint: string]: Array<[string, string]>;
} }
export { LogFunction } from './utils/quote_requestor'; export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void;
export interface SwapQuoterRfqtOpts { export interface SwapQuoterRfqtOpts {
takerApiKeyWhitelist: string[]; takerApiKeyWhitelist: string[];

View File

@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, GetMarketOrdersOpts } from './types'; 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. * Valid sources for market sell.
@ -22,6 +22,7 @@ export const SELL_SOURCE_FILTER = new SourceFilters([
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Swerve, ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.Shell,
ERC20BridgeSource.MultiHop, ERC20BridgeSource.MultiHop,
]); ]);
@ -40,6 +41,7 @@ export const BUY_SOURCE_FILTER = new SourceFilters(
// ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes // ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes
ERC20BridgeSource.MStable, ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Shell,
ERC20BridgeSource.Swerve, ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop, ERC20BridgeSource.MultiHop,
@ -58,8 +60,8 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
sampleDistributionBase: 1.05, sampleDistributionBase: 1.05,
feeSchedule: {}, feeSchedule: {},
gasSchedule: {}, gasSchedule: {},
exchangeProxyOverhead: () => ZERO_AMOUNT,
allowFallback: true, allowFallback: true,
shouldBatchBridgeOrders: true,
shouldGenerateQuoteReport: false, 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 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 * Mainnet Curve configuration
*/ */

View File

@ -3,15 +3,15 @@ import { BigNumber, hexUtils } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types'; import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs // 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; side: MarketOperation;
orders?: SignedOrderWithFillableAmounts[]; orders?: SignedOrderWithFillableAmounts[];
dexQuotes?: DexSample[][]; dexQuotes?: DexSample[][];
@ -28,30 +28,50 @@ export function createFillPaths(opts: {
const dexQuotes = opts.dexQuotes || []; const dexQuotes = opts.dexQuotes || [];
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT;
// Create native fill paths. // Create native fills.
const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, ethToInputRate, feeSchedule); const nativeFills = nativeOrdersToFills(
// Create DEX fill paths. side,
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule); orders,
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources); 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[][] { function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
return paths.filter(path => { const clipped: Fill[] = [];
if (path.length === 0) { let input = ZERO_AMOUNT;
return false; for (const fill of fills) {
if (input.gte(targetInput)) {
break;
} }
const [input, output] = getPathSize(path); input = input.plus(fill.input);
if (input.eq(0) || output.eq(0)) { clipped.push(fill);
return false; }
} return clipped;
if (excludedSources.includes(path[0].source)) {
return false;
}
return true;
});
} }
function nativeOrdersToPath( function hasLiquidity(fills: Fill[]): boolean {
if (fills.length === 0) {
return false;
}
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 nativeOrdersToFills(
side: MarketOperation, side: MarketOperation,
orders: SignedOrderWithFillableAmounts[], orders: SignedOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF, targetInput: BigNumber = POSITIVE_INF,
@ -61,7 +81,7 @@ function nativeOrdersToPath(
): Fill[] { ): Fill[] {
const sourcePathId = hexUtils.random(); const sourcePathId = hexUtils.random();
// Create a single path from all orders. // Create a single path from all orders.
let path: Array<Fill & { adjustedRate: BigNumber }> = []; let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
for (const order of orders) { for (const order of orders) {
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order); const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
@ -87,13 +107,13 @@ function nativeOrdersToPath(
if (adjustedRate.lte(0)) { if (adjustedRate.lte(0)) {
continue; continue;
} }
path.push({ fills.push({
sourcePathId, sourcePathId,
adjustedRate, adjustedRate,
adjustedOutput, adjustedOutput,
input: clippedInput, input: clippedInput,
output: clippedOutput, output: clippedOutput,
flags: 0, flags: SOURCE_FLAGS[ERC20BridgeSource.Native],
index: 0, // TBD index: 0, // TBD
parent: undefined, // TBD parent: undefined, // TBD
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,
@ -101,240 +121,56 @@ function nativeOrdersToPath(
}); });
} }
// Sort by descending adjusted rate. // 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. // Re-index fills.
for (let i = 0; i < path.length; ++i) { for (let i = 0; i < fills.length; ++i) {
path[i].parent = i === 0 ? undefined : path[i - 1]; fills[i].parent = i === 0 ? undefined : fills[i - 1];
path[i].index = i; fills[i].index = i;
} }
return path; return fills;
} }
function dexQuotesToPaths( function dexSamplesToFills(
side: MarketOperation, side: MarketOperation,
dexQuotes: DexSample[][], samples: DexSample[],
ethToOutputRate: BigNumber, ethToOutputRate: BigNumber,
ethToInputRate: BigNumber,
fees: FeeSchedule, fees: FeeSchedule,
): Fill[][] { ): Fill[] {
const paths: Fill[][] = []; const sourcePathId = hexUtils.random();
for (let quote of dexQuotes) { const fills: Fill[] = [];
const sourcePathId = hexUtils.random(); // Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves
const path: Fill[] = []; // We need not worry about Kyber fills going to UniswapReserve as the input amount
// Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves // we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input
// We need not worry about Kyber fills going to UniswapReserve as the input amount // and we only fill [2,3] on Kyber (as 1 returns 0 output)
// we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input const nonzeroSamples = samples.filter(q => !q.output.isZero());
// and we only fill [2,3] on Kyber (as 1 returns 0 output) for (let i = 0; i < nonzeroSamples.length; i++) {
quote = quote.filter(q => !q.output.isZero()); const sample = nonzeroSamples[i];
for (let i = 0; i < quote.length; i++) { const prevSample = i === 0 ? undefined : nonzeroSamples[i - 1];
const sample = quote[i]; const { source, fillData } = sample;
const prevSample = i === 0 ? undefined : quote[i - 1]; const input = sample.input.minus(prevSample ? prevSample.input : 0);
const { source, fillData } = sample; const output = sample.output.minus(prevSample ? prevSample.output : 0);
const input = sample.input.minus(prevSample ? prevSample.input : 0); const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData);
const output = sample.output.minus(prevSample ? prevSample.output : 0); let penalty = ZERO_AMOUNT;
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData); if (i === 0) {
const penalty = // Only the first fill in a DEX path incurs a penalty.
i === 0 // Only the first fill in a DEX path incurs a penalty. penalty = !ethToOutputRate.isZero()
? ethToOutputRate.times(fee) ? 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({
sourcePathId,
input,
output,
adjustedOutput,
source,
fillData,
index: i,
parent: i !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(source),
});
} }
paths.push(path); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
}
return paths;
}
export function getTwoHopAdjustedRate( fills.push({
side: MarketOperation, sourcePathId,
twoHopQuote: DexSample<MultiHopFillData>, input,
targetInput: BigNumber, output,
ethToOutputRate: BigNumber, adjustedOutput,
fees: FeeSchedule = {}, source,
): BigNumber { fillData,
const { output, input, fillData } = twoHopQuote; index: i,
if (input.isLessThan(targetInput) || output.isZero()) { parent: i !== 0 ? fills[fills.length - 1] : undefined,
return ZERO_AMOUNT; flags: SOURCE_FLAGS[source],
}
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; return fills;
}
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);
} }

View File

@ -7,19 +7,19 @@ import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { QuoteRequestor } from '../quote_requestor'; import { QuoteRequestor } from '../quote_requestor';
import { generateQuoteReport } from './../quote_report_generator'; import { generateQuoteReport, QuoteReport } from './../quote_report_generator';
import { import {
BUY_SOURCE_FILTER, BUY_SOURCE_FILTER,
DEFAULT_GET_MARKET_ORDERS_OPTS, DEFAULT_GET_MARKET_ORDERS_OPTS,
FEE_QUOTE_SOURCES, FEE_QUOTE_SOURCES,
ONE_ETHER, ONE_ETHER,
SELL_SOURCE_FILTER, SELL_SOURCE_FILTER,
SOURCE_FLAGS,
ZERO_AMOUNT, ZERO_AMOUNT,
} from './constants'; } from './constants';
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; import { createFills } from './fills';
import { getBestTwoHopQuote } from './multihop_utils'; import { getBestTwoHopQuote } from './multihop_utils';
import { import {
createOrdersFromPath,
createOrdersFromTwoHopSample, createOrdersFromTwoHopSample,
createSignedOrdersFromRfqtIndicativeQuotes, createSignedOrdersFromRfqtIndicativeQuotes,
createSignedOrdersWithFillableAmounts, createSignedOrdersWithFillableAmounts,
@ -30,8 +30,10 @@ import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { import {
AggregationError, AggregationError,
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
ExchangeProxyOverhead,
FeeSchedule, FeeSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketSideLiquidity, MarketSideLiquidity,
@ -78,6 +80,25 @@ export class MarketOperationUtils {
private readonly _buySources: SourceFilters; private readonly _buySources: SourceFilters;
private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES); 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( constructor(
private readonly _sampler: DexOrderSampler, private readonly _sampler: DexOrderSampler,
private readonly contractAddresses: ContractAddresses, private readonly contractAddresses: ContractAddresses,
@ -342,16 +363,26 @@ export class MarketOperationUtils {
): Promise<OptimizerResult> { ): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, { const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage, maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources, excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
allowFallback: _opts.allowFallback, 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> { ): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, { const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage, maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources, excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
allowFallback: _opts.allowFallback, 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, excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
shouldGenerateQuoteReport: false,
}, },
); );
return optimizedOrders; return optimizedOrders;
@ -489,10 +526,9 @@ export class MarketOperationUtils {
maxFallbackSlippage?: number; maxFallbackSlippage?: number;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule; feeSchedule?: FeeSchedule;
exchangeProxyOverhead?: ExchangeProxyOverhead;
allowFallback?: boolean; allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean; shouldBatchBridgeOrders?: boolean;
quoteRequestor?: QuoteRequestor;
shouldGenerateQuoteReport?: boolean;
}, },
): Promise<OptimizerResult> { ): Promise<OptimizerResult> {
const { const {
@ -506,7 +542,6 @@ export class MarketOperationUtils {
dexQuotes, dexQuotes,
ethToOutputRate, ethToOutputRate,
ethToInputRate, ethToInputRate,
twoHopQuotes,
} = marketSideLiquidity; } = marketSideLiquidity;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0; const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
@ -517,11 +552,10 @@ export class MarketOperationUtils {
orderDomain: this._orderDomain, orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses, contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0, bridgeSlippage: opts.bridgeSlippage || 0,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
}; };
// Convert native orders and dex quotes into fill paths. // Convert native orders and dex quotes into `Fill` objects.
const paths = createFillPaths({ const fills = createFills({
side, side,
// Augment native orders with their fillable amounts. // Augment native orders with their fillable amounts.
orders: [ orders: [
@ -537,72 +571,53 @@ export class MarketOperationUtils {
}); });
// Find the optimal path. // Find the optimal path.
let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || []; const optimizerOpts = {
if (optimalPath.length === 0) { 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); throw new Error(AggregationError.NoOptimalPath);
} }
const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount); const optimalPathRate = optimalPath.adjustedRate();
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
marketSideLiquidity, marketSideLiquidity,
opts.feeSchedule, opts.feeSchedule,
opts.exchangeProxyOverhead,
); );
if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) {
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts);
const twoHopQuoteReport = opts.shouldGenerateQuoteReport return {
? generateQuoteReport( optimizedOrders: twoHopOrders,
side, liquidityDelivered: bestTwoHopQuote,
_.flatten(dexQuotes), sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
twoHopQuotes, };
nativeOrders,
orderFillableAmounts,
bestTwoHopQuote,
opts.quoteRequestor,
)
: undefined;
return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true };
} }
// Generate a fallback path if native orders are in the optimal path. // Generate a fallback path if native orders are in the optimal path.
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native); const nativeFills = optimalPath.fills.filter(f => f.source === ERC20BridgeSource.Native);
if (opts.allowFallback && nativeSubPath.length !== 0) { if (opts.allowFallback && nativeFills.length !== 0) {
// We create a fallback path that is exclusive of Native liquidity // We create a fallback path that is exclusive of Native liquidity
// This is the optimal on-chain path for the entire input amount // 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 nonNativeFills = fills.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native);
const nonNativeOptimalPath = const nonNativeOptimalPath = await findOptimalPathAsync(side, nonNativeFills, inputAmount, opts.runLimit);
(await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || [];
// Calculate the slippage of on-chain sources compared to the most optimal path // Calculate the slippage of on-chain sources compared to the most optimal path
const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate); if (
if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) { nonNativeOptimalPath !== undefined &&
// If the last fill is Native and penultimate is not, then the intention was to partial fill (nativeFills.length === optimalPath.fills.length ||
// In this case we drop it entirely as we can't handle a failure at the end and we don't nonNativeOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage)
// want to fully fill when it gets prepended to the front below ) {
const [last, penultimateIfExists] = optimalPath.slice().reverse(); optimalPath.addFallback(nonNativeOptimalPath);
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];
} }
} }
const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts); const collapsedPath = optimalPath.collapse(orderOpts);
const quoteReport = opts.shouldGenerateQuoteReport return {
? generateQuoteReport( optimizedOrders: collapsedPath.orders,
side, liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
_.flatten(dexQuotes), sourceFlags: collapsedPath.sourceFlags,
twoHopQuotes, };
nativeOrders,
orderFillableAmounts,
_.flatten(optimizedOrders.map(order => order.fills)),
opts.quoteRequestor,
)
: undefined;
return { optimizedOrders, quoteReport, isTwoHop: false };
} }
} }

View File

@ -2,8 +2,15 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ZERO_AMOUNT } from './constants'; import { ZERO_AMOUNT } from './constants';
import { getTwoHopAdjustedRate } from './fills'; import { getTwoHopAdjustedRate } from './rate_utils';
import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types'; import {
DexSample,
ExchangeProxyOverhead,
FeeSchedule,
MarketSideLiquidity,
MultiHopFillData,
TokenAdjacencyGraph,
} from './types';
/** /**
* Given a token pair, returns the intermediate tokens to consider for two-hop routes. * Given a token pair, returns the intermediate tokens to consider for two-hop routes.
@ -36,18 +43,28 @@ export function getIntermediateTokens(
export function getBestTwoHopQuote( export function getBestTwoHopQuote(
marketSideLiquidity: MarketSideLiquidity, marketSideLiquidity: MarketSideLiquidity,
feeSchedule?: FeeSchedule, feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity; const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity;
if (twoHopQuotes.length === 0) { if (twoHopQuotes.length === 0) {
return { adjustedRate: ZERO_AMOUNT, quote: undefined }; return { adjustedRate: ZERO_AMOUNT, quote: undefined };
} }
const best = twoHopQuotes const best = twoHopQuotes
.map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule)) .map(quote =>
getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead),
)
.reduce( .reduce(
(prev, curr, i) => (prev, curr, i) =>
curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev, 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], quote: twoHopQuotes[0],
}, },
); );

View File

@ -1,7 +1,7 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
import { RFQTIndicativeQuote } from '@0x/quote-server'; import { RFQTIndicativeQuote } from '@0x/quote-server';
import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
@ -16,7 +16,6 @@ import {
WALLET_SIGNATURE, WALLET_SIGNATURE,
ZERO_AMOUNT, ZERO_AMOUNT,
} from './constants'; } from './constants';
import { collapsePath } from './fills';
import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import { import {
AggregationError, AggregationError,
@ -26,7 +25,6 @@ import {
CurveFillData, CurveFillData,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
KyberFillData, KyberFillData,
LiquidityProviderFillData, LiquidityProviderFillData,
MooniswapFillData, MooniswapFillData,
@ -42,30 +40,6 @@ import {
// tslint:disable completed-docs no-unnecessary-type-assertion // 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( export function createDummyOrderForSampler(
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
@ -152,38 +126,6 @@ export interface CreateOrderFromPathOpts {
orderDomain: OrderDomain; orderDomain: OrderDomain;
contractAddresses: ContractAddresses; contractAddresses: ContractAddresses;
bridgeSlippage: number; 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( export function createOrdersFromTwoHopSample(
@ -242,13 +184,15 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath
return opts.contractAddresses.mStableBridge; return opts.contractAddresses.mStableBridge;
case ERC20BridgeSource.Mooniswap: case ERC20BridgeSource.Mooniswap:
return opts.contractAddresses.mooniswapBridge; return opts.contractAddresses.mooniswapBridge;
case ERC20BridgeSource.Shell:
return opts.contractAddresses.shellBridge;
default: default:
break; break;
} }
throw new Error(AggregationError.NoBridgeForSource); throw new Error(AggregationError.NoBridgeForSource);
} }
function createBridgeOrder( export function createBridgeOrder(
fill: CollapsedFill, fill: CollapsedFill,
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -362,48 +306,7 @@ function createBridgeOrder(
}; };
} }
function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
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] {
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
return [makerToken, takerToken]; return [makerToken, takerToken];
@ -525,7 +428,7 @@ function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOr
}; };
} }
function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
return { return {
fills: [fill], fills: [fill],
...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion

View 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;
}

View File

@ -1,17 +1,9 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { ZERO_AMOUNT } from './constants'; import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path';
import {
arePathFlagsAllowed,
getCompleteRate,
getPathAdjustedCompleteRate,
getPathAdjustedRate,
getPathAdjustedSize,
getPathSize,
isValidPath,
} from './fills';
import { Fill } from './types'; import { Fill } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise // 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; 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. * (for buys) output, while meeting the input requirement.
*/ */
export async function findOptimalPathAsync( export async function findOptimalPathAsync(
side: MarketOperation, side: MarketOperation,
paths: Fill[][], fills: Fill[][],
targetInput: BigNumber, targetInput: BigNumber,
runLimit: number = 2 ** 8, runLimit: number = 2 ** 8,
): Promise<Fill[] | undefined> { opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
// Sort paths by descending adjusted completed rate. ): Promise<Path | undefined> {
const sortedPaths = paths const rates = rateBySourcePathId(side, fills, targetInput);
.slice(0) const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
.sort((a, b) => // Sort fill arrays by descending adjusted completed rate.
getPathAdjustedCompleteRate(side, b, targetInput).comparedTo( const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate()));
getPathAdjustedCompleteRate(side, a, targetInput), if (sortedPaths.length === 0) {
), return undefined;
); }
let optimalPath = sortedPaths[0] || []; let optimalPath = sortedPaths[0];
for (const [i, path] of sortedPaths.slice(1).entries()) { 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. // Yield to event loop.
await Promise.resolve(); await Promise.resolve();
} }
return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; return optimalPath.isComplete() ? optimalPath : undefined;
} }
function mixPaths( function mixPaths(
side: MarketOperation, side: MarketOperation,
pathA: Fill[], pathA: Path,
pathB: Fill[], pathB: Path,
targetInput: BigNumber, targetInput: BigNumber,
maxSteps: number, maxSteps: number,
): Fill[] { rates: { [id: string]: BigNumber },
): Path {
const _maxSteps = Math.max(maxSteps, 32); const _maxSteps = Math.max(maxSteps, 32);
let steps = 0; let steps = 0;
// We assume pathA is the better of the two initially. // We assume pathA is the better of the two initially.
let bestPath: Fill[] = pathA; let bestPath: Path = pathA;
let [bestPathInput, bestPathOutput] = getPathAdjustedSize(pathA, targetInput);
let bestPathRate = getCompleteRate(side, bestPathInput, bestPathOutput, targetInput); const _walk = (path: Path, remainingFills: Fill[]) => {
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[]) => {
steps += 1; steps += 1;
const rate = getCompleteRate(side, input, output, targetInput); if (path.isBetterThan(bestPath)) {
if (_isBetterPath(input, rate)) {
bestPath = path; bestPath = path;
bestPathInput = input;
bestPathOutput = output;
bestPathRate = rate;
} }
const remainingInput = targetInput.minus(input); const remainingInput = targetInput.minus(path.size().input);
if (remainingInput.gt(0)) { if (remainingInput.isGreaterThan(0)) {
for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) { for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) {
const fill = remainingFills[i]; const fill = remainingFills[i];
// Only walk valid paths. // Only walk valid paths.
if (!isValidNextPathFill(path, flags, fill)) { if (!path.isValidNextFill(fill)) {
continue; continue;
} }
// Remove this fill from the next list of candidate fills. // Remove this fill from the next list of candidate fills.
const nextRemainingFills = remainingFills.slice(); const nextRemainingFills = remainingFills.slice();
nextRemainingFills.splice(i, 1); nextRemainingFills.splice(i, 1);
// Recurse. // Recurse.
_walk( _walk(Path.clone(path).append(fill), nextRemainingFills);
[...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,
);
} }
} }
}; };
const allFills = [...pathA, ...pathB]; const allFills = [...pathA.fills, ...pathB.fills];
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),
})),
);
// Sort subpaths by rate and keep fills contiguous to improve our // Sort subpaths by rate and keep fills contiguous to improve our
// chances of walking ideal, valid paths first. // chances of walking ideal, valid paths first.
const sortedFills = allFills.sort((a, b) => { const sortedFills = allFills.sort((a, b) => {
if (a.sourcePathId !== b.sourcePathId) { 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; return a.index - b.index;
}); });
_walk([], ZERO_AMOUNT, ZERO_AMOUNT, 0, sortedFills); _walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills);
if (!isValidPath(bestPath)) { if (!bestPath.isValid()) {
throw new Error('nooope'); throw new Error('nooope');
} }
return bestPath; return bestPath;
} }
function isValidNextPathFill(path: Fill[], pathFlags: number, fill: Fill): boolean { function rateBySourcePathId(
if (path.length === 0) { side: MarketOperation,
return !fill.parent; fills: Fill[][],
} targetInput: BigNumber,
if (path[path.length - 1] === fill.parent) { ): { [id: string]: BigNumber } {
return true; const flattenedFills = _.flatten(fills);
} const sourcePathIds = flattenedFills.filter(f => f.index === 0).map(f => f.sourcePathId);
if (fill.parent) { return Object.assign(
return false; {},
} ...sourcePathIds.map(s => ({
return arePathFlagsAllowed(pathFlags | fill.flags); [s]: Path.create(side, flattenedFills.filter(f => f.sourcePathId === s), targetInput).adjustedRate(),
} })),
);
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);
} }

View File

@ -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);
}

View File

@ -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( public getMedianSellRate(
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
@ -971,6 +997,8 @@ export class SamplerOperations {
.map(poolAddress => .map(poolAddress =>
this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts), this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts),
); );
case ERC20BridgeSource.Shell:
return this.getShellSellQuotes(makerToken, takerToken, takerFillAmounts);
default: default:
throw new Error(`Unsupported sell sample source: ${source}`); throw new Error(`Unsupported sell sample source: ${source}`);
} }
@ -1058,6 +1086,8 @@ export class SamplerOperations {
.map(poolAddress => .map(poolAddress =>
this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts), this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts),
); );
case ERC20BridgeSource.Shell:
return this.getShellBuyQuotes(makerToken, takerToken, makerFillAmounts);
default: default:
throw new Error(`Unsupported buy sample source: ${source}`); throw new Error(`Unsupported buy sample source: ${source}`);
} }

View File

@ -41,6 +41,7 @@ export enum ERC20BridgeSource {
MStable = 'mStable', MStable = 'mStable',
Mooniswap = 'Mooniswap', Mooniswap = 'Mooniswap',
MultiHop = 'MultiHop', MultiHop = 'MultiHop',
Shell = 'Shell',
Swerve = 'Swerve', Swerve = 'Swerve',
SushiSwap = 'SushiSwap', SushiSwap = 'SushiSwap',
} }
@ -156,16 +157,6 @@ export interface DexSample<TFillData extends FillData = FillData> extends Source
output: BigNumber; output: BigNumber;
} }
/**
* Flags for `Fill` objects.
*/
export enum FillFlags {
ConflictsWithKyber = 0x1,
Kyber = 0x2,
ConflictsWithMultiBridge = 0x4,
MultiBridge = 0x8,
}
/** /**
* Represents a node on a fill path. * 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 // 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). // paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string; sourcePathId: string;
// See `FillFlags`. // See `SOURCE_FLAGS`.
flags: FillFlags; flags: number;
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy). // Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
input: BigNumber; input: BigNumber;
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy). // 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 FeeEstimate = (fillData?: FillData) => number | BigNumber;
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber;
/** /**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
@ -288,17 +280,13 @@ export interface GetMarketOrdersOpts {
* Estimated gas consumed by each liquidity source. * Estimated gas consumed by each liquidity source.
*/ */
gasSchedule: FeeSchedule; gasSchedule: FeeSchedule;
exchangeProxyOverhead: ExchangeProxyOverhead;
/** /**
* Whether to pad the quote with a redundant fallback quote using different * Whether to pad the quote with a redundant fallback quote using different
* sources. Defaults to `true`. * sources. Defaults to `true`.
*/ */
allowFallback: boolean; allowFallback: boolean;
rfqt?: GetMarketOrdersRfqtOpts; rfqt?: GetMarketOrdersRfqtOpts;
/**
* Whether to combine contiguous bridge orders into a single DexForwarderBridge
* order. Defaults to `true`.
*/
shouldBatchBridgeOrders: boolean;
/** /**
* Whether to generate a quote report * Whether to generate a quote report
*/ */
@ -321,7 +309,8 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData>
export interface OptimizerResult { export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[]; optimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean; sourceFlags: number;
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
quoteReport?: QuoteReport; quoteReport?: QuoteReport;
} }

View File

@ -64,7 +64,7 @@ export function generateQuoteReport(
multiHopQuotes: Array<DexSample<MultiHopFillData>>, multiHopQuotes: Array<DexSample<MultiHopFillData>>,
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[], orderFillableAmounts: BigNumber[],
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>, liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
quoteRequestor?: QuoteRequestor, quoteRequestor?: QuoteRequestor,
): QuoteReport { ): QuoteReport {
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation)); const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
@ -101,7 +101,9 @@ export function generateQuoteReport(
} }
}); });
} else { } else {
sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)]; sourcesDelivered = [
_multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
];
} }
return { return {
sourcesConsidered, sourcesConsidered,

View File

@ -2,13 +2,13 @@ import { schemas, SchemaValidator } from '@0x/json-schemas';
import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils';
import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server';
import { ERC20AssetData } from '@0x/types'; import { ERC20AssetData } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import Axios, { AxiosInstance } from 'axios'; import Axios, { AxiosInstance } from 'axios';
import { Agent as HttpAgent } from 'http'; import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https'; import { Agent as HttpsAgent } from 'https';
import { constants } from '../constants'; 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 { ONE_SECOND_MS } from './market_operation_utils/constants';
import { RfqMakerBlacklist } from './rfq_maker_blacklist'; 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 { export class QuoteRequestor {
private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); private readonly _schemaValidator: SchemaValidator = new SchemaValidator();
private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {}; private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {};
constructor( constructor(
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
private readonly _warningLogger: LogFunction = (obj, msg) => private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER,
private readonly _infoLogger: LogFunction = (obj, msg) =>
logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`),
private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs, private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs,
) {} ) {
rfqMakerBlacklist.infoLogger = this._infoLogger;
}
public async requestRfqtFirmQuotesAsync( public async requestRfqtFirmQuotesAsync(
makerAssetData: string, makerAssetData: string,
@ -336,31 +334,31 @@ export class QuoteRequestor {
options: RfqtRequestOpts, options: RfqtRequestOpts,
quoteType: 'firm' | 'indicative', quoteType: 'firm' | 'indicative',
): Promise<Array<{ response: ResponseT; makerUri: string }>> { ): Promise<Array<{ response: ResponseT; makerUri: string }>> {
const requestParamsWithBigNumbers = {
takerAddress: options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
};
// convert BigNumbers to strings
// so they are digestible by axios
const requestParams = {
...requestParamsWithBigNumbers,
sellAmountBaseUnits: requestParamsWithBigNumbers.sellAmountBaseUnits
? requestParamsWithBigNumbers.sellAmountBaseUnits.toString()
: undefined,
buyAmountBaseUnits: requestParamsWithBigNumbers.buyAmountBaseUnits
? requestParamsWithBigNumbers.buyAmountBaseUnits.toString()
: undefined,
};
const result: Array<{ response: ResponseT; makerUri: string }> = []; const result: Array<{ response: ResponseT; makerUri: string }> = [];
await Promise.all( await Promise.all(
Object.keys(this._rfqtAssetOfferings).map(async url => { Object.keys(this._rfqtAssetOfferings).map(async url => {
if ( const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url);
this._makerSupportsPair(url, makerAssetData, takerAssetData) && const partialLogEntry = { url, quoteType, requestParams, isBlacklisted };
!rfqMakerBlacklist.isMakerBlacklisted(url) if (isBlacklisted) {
) { this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
const requestParamsWithBigNumbers = { } else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) {
takerAddress: options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
};
// convert BigNumbers to strings
// so they are digestible by axios
const requestParams = {
...requestParamsWithBigNumbers,
sellAmountBaseUnits: requestParamsWithBigNumbers.sellAmountBaseUnits
? requestParamsWithBigNumbers.sellAmountBaseUnits.toString()
: undefined,
buyAmountBaseUnits: requestParamsWithBigNumbers.buyAmountBaseUnits
? requestParamsWithBigNumbers.buyAmountBaseUnits.toString()
: undefined,
};
const partialLogEntry = { url, quoteType, requestParams };
const timeBeforeAwait = Date.now(); const timeBeforeAwait = Date.now();
const maxResponseTimeMs = const maxResponseTimeMs =
options.makerEndpointMaxResponseTimeMs === undefined 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 }); result.push({ response: response.data, makerUri: url });
} catch (err) { } catch (err) {
const latencyMs = Date.now() - timeBeforeAwait; const latencyMs = Date.now() - timeBeforeAwait;
@ -411,7 +409,7 @@ export class QuoteRequestor {
}, },
}, },
}); });
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
this._warningLogger( this._warningLogger(
convertIfAxiosError(err), convertIfAxiosError(err),
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${

View File

@ -349,7 +349,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI
}; };
} }
export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] {
const fills: CollapsedFill[] = []; const fills: CollapsedFill[] = [];
for (const o of orders) { for (const o of orders) {
fills.push(...o.fills); fills.push(...o.fills);

View File

@ -4,11 +4,16 @@
*/ */
import { constants } from '../constants'; import { constants } from '../constants';
import { LogFunction } from '../types';
export class RfqMakerBlacklist { export class RfqMakerBlacklist {
private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {}; private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {};
private readonly _makerBlacklistedUntilDate: { [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 { public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void {
if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) { if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) {
this._makerTimeoutStreakLength[makerUrl] = 0; this._makerTimeoutStreakLength[makerUrl] = 0;
@ -16,8 +21,12 @@ export class RfqMakerBlacklist {
if (didTimeout) { if (didTimeout) {
this._makerTimeoutStreakLength[makerUrl] += 1; this._makerTimeoutStreakLength[makerUrl] += 1;
if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) { if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) {
this._makerBlacklistedUntilDate[makerUrl] = const blacklistEnd = Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS;
Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; this._makerBlacklistedUntilDate[makerUrl] = blacklistEnd;
this.infoLogger(
{ makerUrl, blacklistedUntil: new Date(blacklistEnd).toISOString() },
'maker blacklisted',
);
} }
} else { } else {
this._makerTimeoutStreakLength[makerUrl] = 0; this._makerTimeoutStreakLength[makerUrl] = 0;
@ -27,6 +36,7 @@ export class RfqMakerBlacklist {
const now = Date.now(); const now = Date.now();
if (now > this._makerBlacklistedUntilDate[makerUrl]) { if (now > this._makerBlacklistedUntilDate[makerUrl]) {
delete this._makerBlacklistedUntilDate[makerUrl]; delete this._makerBlacklistedUntilDate[makerUrl];
this.infoLogger({ makerUrl }, 'maker unblacklisted');
} }
return this._makerBlacklistedUntilDate[makerUrl] > now; return this._makerBlacklistedUntilDate[makerUrl] > now;
} }

View File

@ -16,6 +16,7 @@ import {
} from '../types'; } from '../types';
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { SOURCE_FLAGS } from './market_operation_utils/constants';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { import {
ERC20BridgeSource, ERC20BridgeSource,
@ -24,10 +25,9 @@ import {
GetMarketOrdersOpts, GetMarketOrdersOpts,
OptimizedMarketOrder, OptimizedMarketOrder,
} from './market_operation_utils/types'; } from './market_operation_utils/types';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator'; import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator { export class SwapQuoteCalculator {
@ -130,70 +130,75 @@ export class SwapQuoteCalculator {
let optimizedOrders: OptimizedMarketOrder[]; let optimizedOrders: OptimizedMarketOrder[];
let quoteReport: QuoteReport | undefined; let quoteReport: QuoteReport | undefined;
let isTwoHop = false; let sourceFlags: number = 0;
{ // Scale fees by gas price.
// Scale fees by gas price. const _opts: GetMarketOrdersOpts = {
const _opts: GetMarketOrdersOpts = { ...opts,
...opts, feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), ),
), exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)),
}; };
const firstOrderMakerAssetData = !!prunedOrders[0] const firstOrderMakerAssetData = !!prunedOrders[0]
? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData) ? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData)
: { assetProxyId: '' }; : { assetProxyId: '' };
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
} else {
if (operation === MarketOperation.Buy) {
const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders,
assetFillAmount,
_opts,
);
optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport;
sourceFlags = buyResult.sourceFlags;
} else { } else {
if (operation === MarketOperation.Buy) { const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync( prunedOrders,
prunedOrders, assetFillAmount,
assetFillAmount, _opts,
_opts, );
); optimizedOrders = sellResult.optimizedOrders;
optimizedOrders = buyResult.optimizedOrders; quoteReport = sellResult.quoteReport;
quoteReport = buyResult.quoteReport; sourceFlags = sellResult.sourceFlags;
isTwoHop = buyResult.isTwoHop;
} else {
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
assetFillAmount,
_opts,
);
optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport;
isTwoHop = sellResult.isTwoHop;
}
} }
} }
// assetData information for the result // assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0]; const { makerAssetData, takerAssetData } = prunedOrders[0];
return isTwoHop const swapQuote =
? createTwoHopSwapQuote( sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]
makerAssetData, ? createTwoHopSwapQuote(
takerAssetData, makerAssetData,
optimizedOrders, takerAssetData,
operation, optimizedOrders,
assetFillAmount, operation,
gasPrice, assetFillAmount,
opts.gasSchedule, gasPrice,
quoteReport, opts.gasSchedule,
) quoteReport,
: createSwapQuote( )
makerAssetData, : createSwapQuote(
takerAssetData, makerAssetData,
optimizedOrders, takerAssetData,
operation, optimizedOrders,
assetFillAmount, operation,
gasPrice, assetFillAmount,
opts.gasSchedule, gasPrice,
quoteReport, 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;
} }
} }

View File

@ -21,6 +21,7 @@ import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquid
import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json';
import * as IMStable from '../test/generated-artifacts/IMStable.json'; import * as IMStable from '../test/generated-artifacts/IMStable.json';
import * as IMultiBridge from '../test/generated-artifacts/IMultiBridge.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 IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json';
import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json';
import * as KyberSampler from '../test/generated-artifacts/KyberSampler.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 MultiBridgeSampler from '../test/generated-artifacts/MultiBridgeSampler.json';
import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSampler.json'; import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSampler.json';
import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.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 SushiSwapSampler from '../test/generated-artifacts/SushiSwapSampler.json';
import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json'; import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json';
@ -50,6 +52,7 @@ export const artifacts = {
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact, NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact, SamplerUtils: SamplerUtils as ContractArtifact,
ShellSampler: ShellSampler as ContractArtifact,
SushiSwapSampler: SushiSwapSampler as ContractArtifact, SushiSwapSampler: SushiSwapSampler as ContractArtifact,
TwoHopSampler: TwoHopSampler as ContractArtifact, TwoHopSampler: TwoHopSampler as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact, UniswapSampler: UniswapSampler as ContractArtifact,
@ -62,6 +65,7 @@ export const artifacts = {
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IMStable: IMStable as ContractArtifact, IMStable: IMStable as ContractArtifact,
IMultiBridge: IMultiBridge as ContractArtifact, IMultiBridge: IMultiBridge as ContractArtifact,
IShell: IShell as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,

View File

@ -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),
);
});
});
});
});

View File

@ -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
});

View File

@ -22,9 +22,10 @@ import {
BUY_SOURCE_FILTER, BUY_SOURCE_FILTER,
POSITIVE_INF, POSITIVE_INF,
SELL_SOURCE_FILTER, SELL_SOURCE_FILTER,
SOURCE_FLAGS,
ZERO_AMOUNT, ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants'; } 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 { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import { import {
@ -49,6 +50,7 @@ const DEFAULT_EXCLUDED = [
ERC20BridgeSource.Swerve, ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop, ERC20BridgeSource.MultiHop,
ERC20BridgeSource.Shell,
]; ];
const BUY_SOURCES = BUY_SOURCE_FILTER.sources; const BUY_SOURCES = BUY_SOURCE_FILTER.sources;
const SELL_SOURCES = SELL_SOURCE_FILTER.sources; const SELL_SOURCES = SELL_SOURCE_FILTER.sources;
@ -107,6 +109,8 @@ describe('MarketOperationUtils tests', () => {
return ERC20BridgeSource.Mooniswap; return ERC20BridgeSource.Mooniswap;
case contractAddresses.sushiswapBridge.toLowerCase(): case contractAddresses.sushiswapBridge.toLowerCase():
return ERC20BridgeSource.SushiSwap; return ERC20BridgeSource.SushiSwap;
case contractAddresses.shellBridge.toLowerCase():
return ERC20BridgeSource.Shell;
default: default:
break; break;
} }
@ -261,27 +265,6 @@ describe('MarketOperationUtils tests', () => {
return rates; 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; const NUM_SAMPLES = 3;
interface RatesBySource { interface RatesBySource {
@ -304,6 +287,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Swerve]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Swerve]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.SushiSwap]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.SushiSwap]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.MultiHop]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.MultiHop]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.Shell]: _.times(NUM_SAMPLES, () => 0),
}; };
const DEFAULT_RATES: RatesBySource = { const DEFAULT_RATES: RatesBySource = {
@ -349,6 +333,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() },
[ERC20BridgeSource.Native]: { order: createOrder() }, [ERC20BridgeSource.Native]: { order: createOrder() },
[ERC20BridgeSource.MultiHop]: {}, [ERC20BridgeSource.MultiHop]: {},
[ERC20BridgeSource.Shell]: {},
}; };
const DEFAULT_OPS = { const DEFAULT_OPS = {
@ -466,7 +451,6 @@ describe('MarketOperationUtils tests', () => {
maxFallbackSlippage: 100, maxFallbackSlippage: 100,
excludedSources: DEFAULT_EXCLUDED, excludedSources: DEFAULT_EXCLUDED,
allowFallback: false, allowFallback: false,
shouldBatchBridgeOrders: false,
}; };
beforeEach(() => { beforeEach(() => {
@ -881,7 +865,6 @@ describe('MarketOperationUtils tests', () => {
excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor), excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor),
numSamples: 4, numSamples: 4,
bridgeSlippage: 0, bridgeSlippage: 0,
shouldBatchBridgeOrders: false,
}, },
); );
const result = ordersAndReport.optimizedOrders; const result = ordersAndReport.optimizedOrders;
@ -899,36 +882,48 @@ describe('MarketOperationUtils tests', () => {
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
}); });
it('batches contiguous bridge sources', async () => { it('factors in exchange proxy gas overhead', async () => {
const rates: RatesBySource = {}; // Uniswap has a slightly better rate than LiquidityProvider,
rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; // but LiquidityProvider is better accounting for the EP gas overhead.
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; const rates: RatesBySource = {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; [ERC20BridgeSource.Native]: [0.01, 0.01, 0.01, 0.01],
rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; [ERC20BridgeSource.Uniswap]: [1, 1, 1, 1],
[ERC20BridgeSource.LiquidityProvider]: [0.9999, 0.9999, 0.9999, 0.9999],
};
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), 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]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
numSamples: 4, numSamples: 4,
excludedSources: [ excludedSources: [
...DEFAULT_OPTS.excludedSources,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve), ERC20BridgeSource.Bancor,
], ],
shouldBatchBridgeOrders: true, exchangeProxyOverhead,
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(3); const orderSources = improvedOrders.map(o => o.fills[0].source);
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); const expectedSources = [ERC20BridgeSource.LiquidityProvider];
expect(orderFillSources).to.deep.eq([ expect(orderSources).to.deep.eq(expectedSources);
[ERC20BridgeSource.Uniswap],
[ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve],
]);
}); });
}); });
@ -945,7 +940,6 @@ describe('MarketOperationUtils tests', () => {
maxFallbackSlippage: 100, maxFallbackSlippage: 100,
excludedSources: DEFAULT_EXCLUDED, excludedSources: DEFAULT_EXCLUDED,
allowFallback: false, allowFallback: false,
shouldBatchBridgeOrders: false,
}; };
beforeEach(() => { beforeEach(() => {
@ -1297,35 +1291,52 @@ describe('MarketOperationUtils tests', () => {
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
}); });
it('batches contiguous bridge sources', async () => { it('factors in exchange proxy gas overhead', async () => {
const rates: RatesBySource = { ...ZERO_RATES }; // Uniswap has a slightly better rate than LiquidityProvider,
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; // but LiquidityProvider is better accounting for the EP gas overhead.
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; const rates: RatesBySource = {
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; [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({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const optimizer = new MarketOperationUtils(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), 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, FILL_AMOUNT,
{ {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
numSamples: 4, numSamples: 4,
shouldBatchBridgeOrders: true, excludedSources: [
...DEFAULT_OPTS.excludedSources,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
],
exchangeProxyOverhead,
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(2); const orderSources = improvedOrders.map(o => o.fills[0].source);
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); const expectedSources = [ERC20BridgeSource.LiquidityProvider];
expect(orderFillSources).to.deep.eq([ expect(orderSources).to.deep.eq(expectedSources);
[ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
]);
}); });
}); });
}); });
describe('createFillPaths', () => { describe('createFills', () => {
const takerAssetAmount = new BigNumber(5000000); const takerAssetAmount = new BigNumber(5000000);
const ethToOutputRate = new BigNumber(0.5); const ethToOutputRate = new BigNumber(0.5);
// tslint:disable-next-line:no-object-literal-type-assertion // 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', () => { it('penalizes native fill based on target amount when target is smaller', () => {
const path = createFillPaths({ const path = createFills({
side: MarketOperation.Sell, side: MarketOperation.Sell,
orders, orders,
dexQuotes: [], dexQuotes: [],
@ -1372,7 +1383,7 @@ describe('MarketOperationUtils tests', () => {
}); });
it('penalizes native fill based on available amount when target is larger', () => { it('penalizes native fill based on available amount when target is larger', () => {
const path = createFillPaths({ const path = createFills({
side: MarketOperation.Sell, side: MarketOperation.Sell,
orders, orders,
dexQuotes: [], dexQuotes: [],

View File

@ -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_m_stable';
export * from '../test/generated-wrappers/i_mooniswap'; export * from '../test/generated-wrappers/i_mooniswap';
export * from '../test/generated-wrappers/i_multi_bridge'; 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_exchange_quotes';
export * from '../test/generated-wrappers/i_uniswap_v2_router01'; export * from '../test/generated-wrappers/i_uniswap_v2_router01';
export * from '../test/generated-wrappers/kyber_sampler'; 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/multi_bridge_sampler';
export * from '../test/generated-wrappers/native_order_sampler'; export * from '../test/generated-wrappers/native_order_sampler';
export * from '../test/generated-wrappers/sampler_utils'; 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/sushi_swap_sampler';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler';
export * from '../test/generated-wrappers/test_native_order_sampler'; export * from '../test/generated-wrappers/test_native_order_sampler';

View File

@ -24,6 +24,7 @@
"test/generated-artifacts/IMStable.json", "test/generated-artifacts/IMStable.json",
"test/generated-artifacts/IMooniswap.json", "test/generated-artifacts/IMooniswap.json",
"test/generated-artifacts/IMultiBridge.json", "test/generated-artifacts/IMultiBridge.json",
"test/generated-artifacts/IShell.json",
"test/generated-artifacts/IUniswapExchangeQuotes.json", "test/generated-artifacts/IUniswapExchangeQuotes.json",
"test/generated-artifacts/IUniswapV2Router01.json", "test/generated-artifacts/IUniswapV2Router01.json",
"test/generated-artifacts/KyberSampler.json", "test/generated-artifacts/KyberSampler.json",
@ -33,6 +34,7 @@
"test/generated-artifacts/MultiBridgeSampler.json", "test/generated-artifacts/MultiBridgeSampler.json",
"test/generated-artifacts/NativeOrderSampler.json", "test/generated-artifacts/NativeOrderSampler.json",
"test/generated-artifacts/SamplerUtils.json", "test/generated-artifacts/SamplerUtils.json",
"test/generated-artifacts/ShellSampler.json",
"test/generated-artifacts/SushiSwapSampler.json", "test/generated-artifacts/SushiSwapSampler.json",
"test/generated-artifacts/TestERC20BridgeSampler.json", "test/generated-artifacts/TestERC20BridgeSampler.json",
"test/generated-artifacts/TestNativeOrderSampler.json", "test/generated-artifacts/TestNativeOrderSampler.json",

View File

@ -49,6 +49,10 @@
{ {
"note": "Deploy `BancorBridge` on Mainnet", "note": "Deploy `BancorBridge` on Mainnet",
"pr": 2699 "pr": 2699
},
{
"note": "Deploy `ShellBridge` on Mainnet",
"pr": 2722
} }
] ]
}, },

View File

@ -43,6 +43,7 @@
"mStableBridge": "0x2bf04fcea05f0989a14d9afa37aa376baca6b2b3", "mStableBridge": "0x2bf04fcea05f0989a14d9afa37aa376baca6b2b3",
"mooniswapBridge": "0x02b7eca484ad960fca3f7709e0b2ac81eec3069c", "mooniswapBridge": "0x02b7eca484ad960fca3f7709e0b2ac81eec3069c",
"sushiswapBridge": "0x47ed0262a0b688dcb836d254c6a2e96b6c48a9f5", "sushiswapBridge": "0x47ed0262a0b688dcb836d254c6a2e96b6c48a9f5",
"shellBridge": "0x21fb3862eed7911e0f8219a077247b849846728d",
"transformers": { "transformers": {
"wethTransformer": "0x68c0bb685099dc7cb5c5ce2b26185945b357383e", "wethTransformer": "0x68c0bb685099dc7cb5c5ce2b26185945b357383e",
"payTakerTransformer": "0x49b9df2c58491764cf40cb052dd4243df63622c7", "payTakerTransformer": "0x49b9df2c58491764cf40cb052dd4243df63622c7",
@ -94,6 +95,7 @@
"mStableBridge": "0x0000000000000000000000000000000000000000", "mStableBridge": "0x0000000000000000000000000000000000000000",
"mooniswapBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000",
"sushiswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000",
"shellBridge": "0x0000000000000000000000000000000000000000",
"transformers": { "transformers": {
"wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437",
"payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6",
@ -145,6 +147,7 @@
"mStableBridge": "0x0000000000000000000000000000000000000000", "mStableBridge": "0x0000000000000000000000000000000000000000",
"mooniswapBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000",
"sushiswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000",
"shellBridge": "0x0000000000000000000000000000000000000000",
"transformers": { "transformers": {
"wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437",
"payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6",
@ -196,6 +199,7 @@
"mStableBridge": "0x0000000000000000000000000000000000000000", "mStableBridge": "0x0000000000000000000000000000000000000000",
"mooniswapBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000",
"sushiswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000",
"shellBridge": "0x0000000000000000000000000000000000000000",
"transformers": { "transformers": {
"wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d",
"payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977",
@ -247,6 +251,7 @@
"mStableBridge": "0x0000000000000000000000000000000000000000", "mStableBridge": "0x0000000000000000000000000000000000000000",
"mooniswapBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000",
"sushiswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000",
"shellBridge": "0x0000000000000000000000000000000000000000",
"transformers": { "transformers": {
"wethTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5", "wethTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5",
"payTakerTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", "payTakerTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3",

View File

@ -44,6 +44,7 @@ export interface ContractAddresses {
mStableBridge: string; mStableBridge: string;
mooniswapBridge: string; mooniswapBridge: string;
sushiswapBridge: string; sushiswapBridge: string;
shellBridge: string;
transformers: { transformers: {
wethTransformer: string; wethTransformer: string;
payTakerTransformer: string; payTakerTransformer: string;

View File

@ -17,6 +17,10 @@
{ {
"note": "Regenerate artifacts", "note": "Regenerate artifacts",
"pr": 2703 "pr": 2703
},
{
"note": "Update IZeroEx artifact for LiquidityProviderFeature",
"pr": 2691
} }
] ]
}, },

View File

@ -3,6 +3,16 @@
"contractName": "IZeroEx", "contractName": "IZeroEx",
"compilerOutput": { "compilerOutput": {
"abi": [ "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, "anonymous": false,
"inputs": [ "inputs": [
@ -222,6 +232,16 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "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": [ "inputs": [
{ {
@ -366,6 +386,19 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "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": [ "inputs": [
{ "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" }, { "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" },
@ -378,6 +411,17 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "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" }], "inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }],
"name": "setQuoteSigner", "name": "setQuoteSigner",
@ -492,6 +536,14 @@
"params": { "selector": "The function selector." }, "params": { "selector": "The function selector." },
"returns": { "impl": "The implementation contract address." } "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))": { "getMetaTransactionExecutedBlock((address,address,uint256,uint256,uint256,uint256,bytes,uint256,address,uint256))": {
"details": "Get the block at which a meta-transaction has been executed.", "details": "Get the block at which a meta-transaction has been executed.",
"params": { "mtx": "The meta-transaction." }, "params": { "mtx": "The meta-transaction." },
@ -574,6 +626,14 @@
}, },
"returns": { "buyAmount": "Amount of `tokens[-1]` bought." } "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)": { "setQuoteSigner(address)": {
"details": "Replace the optional signer for `transformERC20()` calldata. Only callable by the owner.", "details": "Replace the optional signer for `transformERC20()` calldata. Only callable by the owner.",
"params": { "quoteSigner": "The address of the new calldata signer." } "params": { "quoteSigner": "The address of the new calldata signer." }

View File

@ -17,6 +17,10 @@
{ {
"note": "Regenerate wrappers", "note": "Regenerate wrappers",
"pr": 2703 "pr": 2703
},
{
"note": "Update IZeroEx wrapper for LiquidityProviderFeature",
"pr": 2691
} }
] ]
}, },

View File

@ -36,6 +36,7 @@ import * as ethers from 'ethers';
// tslint:enable:no-unused-variable // tslint:enable:no-unused-variable
export type IZeroExEventArgs = export type IZeroExEventArgs =
| IZeroExLiquidityProviderForMarketUpdatedEventArgs
| IZeroExMetaTransactionExecutedEventArgs | IZeroExMetaTransactionExecutedEventArgs
| IZeroExMigratedEventArgs | IZeroExMigratedEventArgs
| IZeroExOwnershipTransferredEventArgs | IZeroExOwnershipTransferredEventArgs
@ -45,6 +46,7 @@ export type IZeroExEventArgs =
| IZeroExTransformerDeployerUpdatedEventArgs; | IZeroExTransformerDeployerUpdatedEventArgs;
export enum IZeroExEvents { export enum IZeroExEvents {
LiquidityProviderForMarketUpdated = 'LiquidityProviderForMarketUpdated',
MetaTransactionExecuted = 'MetaTransactionExecuted', MetaTransactionExecuted = 'MetaTransactionExecuted',
Migrated = 'Migrated', Migrated = 'Migrated',
OwnershipTransferred = 'OwnershipTransferred', OwnershipTransferred = 'OwnershipTransferred',
@ -54,6 +56,12 @@ export enum IZeroExEvents {
TransformerDeployerUpdated = 'TransformerDeployerUpdated', TransformerDeployerUpdated = 'TransformerDeployerUpdated',
} }
export interface IZeroExLiquidityProviderForMarketUpdatedEventArgs extends DecodedLogArgs {
xAsset: string;
yAsset: string;
providerAddress: string;
}
export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs { export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs {
hash: string; hash: string;
selector: string; selector: string;
@ -211,6 +219,29 @@ export class IZeroExContract extends BaseContract {
*/ */
public static ABI(): ContractAbi { public static ABI(): ContractAbi {
const abi = [ 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, anonymous: false,
inputs: [ inputs: [
@ -697,6 +728,27 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'view', stateMutability: 'view',
type: 'function', type: 'function',
}, },
{
inputs: [
{
name: 'xAsset',
type: 'address',
},
{
name: 'yAsset',
type: 'address',
},
],
name: 'getLiquidityProviderForMarket',
outputs: [
{
name: 'providerAddress',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{ {
inputs: [ inputs: [
{ {
@ -1000,6 +1052,39 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'nonpayable', stateMutability: 'nonpayable',
type: 'function', 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: [ inputs: [
{ {
@ -1029,6 +1114,26 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'payable', stateMutability: 'payable',
type: 'function', type: 'function',
}, },
{
inputs: [
{
name: 'xAsset',
type: 'address',
},
{
name: 'yAsset',
type: 'address',
},
{
name: 'providerAddress',
type: 'address',
},
],
name: 'setLiquidityProviderForMarket',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{ {
inputs: [ 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. * Get the block at which a meta-transaction has been executed.
* @param mtx The meta-transaction. * @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. * Efficiently sell directly to uniswap/sushiswap.
* @param tokens Sell path. * @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. * Replace the optional signer for `transformERC20()` calldata.
* Only callable by the owner. * Only callable by the owner.

View File

@ -125,6 +125,7 @@ export {
IZeroExContract, IZeroExContract,
IZeroExEventArgs, IZeroExEventArgs,
IZeroExEvents, IZeroExEvents,
IZeroExLiquidityProviderForMarketUpdatedEventArgs,
IZeroExMetaTransactionExecutedEventArgs, IZeroExMetaTransactionExecutedEventArgs,
IZeroExMigratedEventArgs, IZeroExMigratedEventArgs,
IZeroExOwnershipTransferredEventArgs, IZeroExOwnershipTransferredEventArgs,

View File

@ -324,6 +324,8 @@ export async function runMigrationsAsync(
uniswapV2Router: NULL_ADDRESS, uniswapV2Router: NULL_ADDRESS,
uniswapExchangeFactory: NULL_ADDRESS, uniswapExchangeFactory: NULL_ADDRESS,
mStable: NULL_ADDRESS, mStable: NULL_ADDRESS,
shellBridge: NULL_ADDRESS,
shell: NULL_ADDRESS,
weth: etherToken.address, weth: etherToken.address,
}, },
); );
@ -401,6 +403,7 @@ export async function runMigrationsAsync(
mStableBridge: NULL_ADDRESS, mStableBridge: NULL_ADDRESS,
mooniswapBridge: NULL_ADDRESS, mooniswapBridge: NULL_ADDRESS,
sushiswapBridge: NULL_ADDRESS, sushiswapBridge: NULL_ADDRESS,
shellBridge: NULL_ADDRESS,
exchangeProxy: exchangeProxy.address, exchangeProxy: exchangeProxy.address,
exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress,
exchangeProxyTransformerDeployer: txDefaults.from, exchangeProxyTransformerDeployer: txDefaults.from,

View File

@ -9,6 +9,14 @@
{ {
"note": "Add EP flavor of `IllegalReentrancyError`.", "note": "Add EP flavor of `IllegalReentrancyError`.",
"pr": 2657 "pr": 2657
},
{
"note": "Added LiquidityProviderFeature errors",
"pr": 2691
},
{
"note": "Added abi encoder support for uint80 lol",
"pr": 2728
} }
] ]
}, },

View File

@ -10,7 +10,7 @@ import * as EncoderMath from '../utils/math';
export class UIntDataType extends AbstractBlobDataType { export class UIntDataType extends AbstractBlobDataType {
private static readonly _MATCHER = RegExp( 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 _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
private static readonly _MAX_WIDTH: number = 256; private static readonly _MAX_WIDTH: number = 256;

View File

@ -54,4 +54,5 @@ export const ZeroExRevertErrors = {
Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'), Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'),
MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'), MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'),
SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'), SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'),
LiquidityProvider: require('./revert_errors/zero-ex/liquidity_provider_revert_errors'),
}; };

View File

@ -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);
}