UniswapFeature (#2703)

* Minimal Uniswap accessor.

* Add comments

* Safe math

* mainnet gas benchmark

* Assembler Uniswap

* Selectors and addresses

* Fix bugs in ABI encoders

* Typo

* AsmUniswap test

* Fix wantAmount computation

* Golfing

* Bypass AllowanceTarget

* Generalized asm uniswapper

* Implement ordering

* Fix pair computation

* #6 Golfing Iron

* Remove 'to' argument (saves 377 gas)

* New contract api

* `@0x/contracts-zero-ex`: Add `UniswapFeature`

* `@0x/contract-artifacts`: Regenerate artifacts

* `@0x/contract-wrappers`: Regenerate wrappers

* `@0x/asset-swapper`: Add Uniswap VIP support.
`@0x/asset-swapper`: Add `includeSources` support.

* `@0x/contracts-zero-ex`: Fix misleading comments in `UniswapFeature`.
`@0x/asset-swapper`: Fix linter errors.

* `@0x/asset-swapper`: Fix source filter bugs.

* `@0x/contracts-zero-ex`: `UniswapFeature`: Reduce calldata size for AllowanceTarget call
`@0x/asset-swapper`: Fix failing test.

* `@0x/contracts-zero-ex`: Fix ETH buy tokens not being normalized to WETH.

* `@0x/asset-swapper`: Fix multi-hop weirdness with source filters.

* `@0x/asset-swapper`: Fix failing test.

* `@0x/asset-swapper`: Really fix that broken AS test.

* `@0x/asset-swapper`: use filter objects instead of source array for valid buy and sell sources/

* `@0x/asset-swapper`: Move some source filtering logic into the sampler operations.

* `@0x/contracts-zero-ex`: Address PR feedback

* `@0x/contracts-zero-ex`: Fix feature version bug.

* `@0x/asset-swapper`: Did I actually fix AS tests this time? Who knows.

Co-authored-by: Remco Bloemen <remco@0x.org>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Lawrence Forman 2020-09-23 02:27:48 -04:00 committed by GitHub
parent 32d11d1ba5
commit f84b375cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1044 additions and 179 deletions

View File

@ -49,6 +49,14 @@
{ {
"note": "Add updated Kyber and Mooniswap rollup to FQT", "note": "Add updated Kyber and Mooniswap rollup to FQT",
"pr": 2692 "pr": 2692
},
{
"note": "Add `UniswapFeature`",
"pr": 2703
},
{
"note": "Fix versioning (`_encodeVersion()`) bug",
"pr": 2703
} }
] ]
}, },

View File

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

View File

@ -0,0 +1,43 @@
/*
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";
/// @dev VIP uniswap fill functions.
interface IUniswapFeature {
/// @dev Efficiently sell directly to uniswap/sushiswap.
/// @param tokens Sell path.
/// @param sellAmount of `tokens[0]` Amount to sell.
/// @param minBuyAmount Minimum amount of `tokens[-1]` to buy.
/// @param isSushi Use sushiswap if true.
/// @return buyAmount Amount of `tokens[-1]` bought.
function sellToUniswap(
IERC20TokenV06[] calldata tokens,
uint256 sellAmount,
uint256 minBuyAmount,
bool isSushi
)
external
payable
returns (uint256 buyAmount);
}

View File

@ -0,0 +1,366 @@
/*
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 "../migrations/LibMigrate.sol";
import "../external/IAllowanceTarget.sol";
import "../fixins/FixinCommon.sol";
import "./IFeature.sol";
import "./IUniswapFeature.sol";
/// @dev VIP uniswap fill functions.
contract UniswapFeature is
IFeature,
IUniswapFeature,
FixinCommon
{
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "UniswapFeature";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev WETH contract.
IEtherTokenV06 private immutable WETH;
/// @dev AllowanceTarget instance.
IAllowanceTarget private immutable ALLOWANCE_TARGET;
// 0xFF + address of the UniswapV2Factory contract.
uint256 constant private FF_UNISWAP_FACTORY = 0xFF5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f0000000000000000000000;
// 0xFF + address of the (Sushiswap) UniswapV2Factory contract.
uint256 constant private FF_SUSHISWAP_FACTORY = 0xFFC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac0000000000000000000000;
// Init code hash of the UniswapV2Pair contract.
uint256 constant private UNISWAP_PAIR_INIT_CODE_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
// Init code hash of the (Sushiswap) UniswapV2Pair contract.
uint256 constant private SUSHISWAP_PAIR_INIT_CODE_HASH = 0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303;
// Mask of the lower 20 bytes of a bytes32.
uint256 constant private ADDRESS_MASK = 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff;
// ETH pseudo-token address.
uint256 constant private ETH_TOKEN_ADDRESS_32 = 0x000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee;
// Maximum token quantity that can be swapped against the UniswapV2Pair contract.
uint256 constant private MAX_SWAP_AMOUNT = 2**112;
// bytes4(keccak256("executeCall(address,bytes)"))
uint256 constant private ALLOWANCE_TARGET_EXECUTE_CALL_SELECTOR_32 = 0xbca8c7b500000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("getReserves()"))
uint256 constant private UNISWAP_PAIR_RESERVES_CALL_SELECTOR_32 = 0x0902f1ac00000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("swap(uint256,uint256,address,bytes)"))
uint256 constant private UNISWAP_PAIR_SWAP_CALL_SELECTOR_32 = 0x022c0d9f00000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("transferFrom(address,address,uint256)"))
uint256 constant private TRANSFER_FROM_CALL_SELECTOR_32 = 0x23b872dd00000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("withdraw(uint256)"))
uint256 constant private WETH_WITHDRAW_CALL_SELECTOR_32 = 0x2e1a7d4d00000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("deposit()"))
uint256 constant private WETH_DEPOSIT_CALL_SELECTOR_32 = 0xd0e30db000000000000000000000000000000000000000000000000000000000;
// bytes4(keccak256("transfer(address,uint256)"))
uint256 constant private ERC20_TRANSFER_CALL_SELECTOR_32 = 0xa9059cbb00000000000000000000000000000000000000000000000000000000;
/// @dev Construct this contract.
/// @param weth The WETH contract.
/// @param allowanceTarget The AllowanceTarget contract.
constructor(IEtherTokenV06 weth, IAllowanceTarget allowanceTarget) public {
WETH = weth;
ALLOWANCE_TARGET = allowanceTarget;
}
/// @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.sellToUniswap.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Efficiently sell directly to uniswap/sushiswap.
/// @param tokens Sell path.
/// @param sellAmount of `tokens[0]` Amount to sell.
/// @param minBuyAmount Minimum amount of `tokens[-1]` to buy.
/// @param isSushi Use sushiswap if true.
/// @return buyAmount Amount of `tokens[-1]` bought.
function sellToUniswap(
IERC20TokenV06[] calldata tokens,
uint256 sellAmount,
uint256 minBuyAmount,
bool isSushi
)
external
payable
override
returns (uint256 buyAmount)
{
require(tokens.length > 1, "UniswapFeature/InvalidTokensLength");
{
// Load immutables onto the stack.
IEtherTokenV06 weth = WETH;
IAllowanceTarget allowanceTarget = ALLOWANCE_TARGET;
// Store some vars in memory to get around stack limits.
assembly {
// calldataload(mload(0xA00)) == first element of `tokens` array
mstore(0xA00, add(calldataload(0x04), 0x24))
// mload(0xA20) == isSushi
mstore(0xA20, isSushi)
// mload(0xA40) == WETH
mstore(0xA40, weth)
// mload(0xA60) == ALLOWANCE_TARGET
mstore(0xA60, allowanceTarget)
}
}
assembly {
// numPairs == tokens.length - 1
let numPairs := sub(calldataload(add(calldataload(0x04), 0x4)), 1)
// We use the previous buy amount as the sell amount for the next
// pair in a path. So for the first swap we want to set it to `sellAmount`.
buyAmount := sellAmount
let buyToken
let nextPair := 0
for {let i := 0} lt(i, numPairs) {i := add(i, 1)} {
// sellToken = tokens[i]
let sellToken := loadTokenAddress(i)
// buyToken = tokens[i+1]
buyToken := loadTokenAddress(add(i, 1))
// The canonical ordering of this token pair.
let pairOrder := lt(normalizeToken(sellToken), normalizeToken(buyToken))
// Compute the pair address if it hasn't already been computed
// from the last iteration.
let pair := nextPair
if iszero(pair) {
pair := computePairAddress(sellToken, buyToken)
nextPair := 0
}
if iszero(i) {
switch eq(sellToken, ETH_TOKEN_ADDRESS_32)
case 0 {
// For the first pair we need to transfer sellTokens into the
// pair contract using `AllowanceTarget.executeCall()`
mstore(0xB00, ALLOWANCE_TARGET_EXECUTE_CALL_SELECTOR_32)
mstore(0xB04, sellToken)
mstore(0xB24, 0x40)
mstore(0xB44, 0x64)
mstore(0xB64, TRANSFER_FROM_CALL_SELECTOR_32)
mstore(0xB68, caller())
mstore(0xB88, pair)
mstore(0xBA8, sellAmount)
if iszero(call(gas(), mload(0xA60), 0, 0xB00, 0xC8, 0x00, 0x0)) {
bubbleRevert()
}
}
default {
// If selling ETH, we need to wrap it to WETH and transfer to the
// pair contract.
if iszero(eq(callvalue(), sellAmount)) {
revert(0, 0)
}
sellToken := mload(0xA40)// Re-assign to WETH
// Call `WETH.deposit{value: sellAmount}()`
mstore(0xB00, WETH_DEPOSIT_CALL_SELECTOR_32)
if iszero(call(gas(), sellToken, sellAmount, 0xB00, 0x4, 0x00, 0x0)) {
bubbleRevert()
}
// Call `WETH.transfer(pair, sellAmount)`
mstore(0xB00, ERC20_TRANSFER_CALL_SELECTOR_32)
mstore(0xB04, pair)
mstore(0xB24, sellAmount)
if iszero(call(gas(), sellToken, 0, 0xB00, 0x44, 0x00, 0x0)) {
bubbleRevert()
}
}
// No need to check results, if deposit/transfers failed the UniswapV2Pair will
// reject our trade (or it may succeed if somehow the reserve was out of sync)
// this is fine for the taker.
}
// Call pair.getReserves(), store the results at `0xC00`
mstore(0xB00, UNISWAP_PAIR_RESERVES_CALL_SELECTOR_32)
if iszero(staticcall(gas(), pair, 0xB00, 0x4, 0xC00, 0x40)) {
bubbleRevert()
}
// Sell amount for this hop is the previous buy amount.
let pairSellAmount := buyAmount
// Compute the buy amount based on the pair reserves.
{
let sellReserve
let buyReserve
switch iszero(pairOrder)
case 0 {
// Transpose if pair order is different.
sellReserve := mload(0xC00)
buyReserve := mload(0xC20)
}
default {
sellReserve := mload(0xC20)
buyReserve := mload(0xC00)
}
// Ensure that the sellAmount is < 2¹¹².
if gt(pairSellAmount, MAX_SWAP_AMOUNT) {
revert(0, 0)
}
// Pairs are in the range (0, 2¹¹²) so this shouldn't overflow.
// buyAmount = (pairSellAmount * 997 * buyReserve) /
// (pairSellAmount * 997 + sellReserve * 1000);
let sellAmountWithFee := mul(pairSellAmount, 997)
buyAmount := div(
mul(sellAmountWithFee, buyReserve),
add(sellAmountWithFee, mul(sellReserve, 1000))
)
}
let receiver
// Is this the last pair contract?
switch eq(add(i, 1), numPairs)
case 0 {
// Not the last pair contract, so forward bought tokens to
// the next pair contract.
nextPair := computePairAddress(
buyToken,
loadTokenAddress(add(i, 2))
)
receiver := nextPair
}
default {
// The last pair contract.
// Forward directly to taker UNLESS they want ETH back.
switch eq(buyToken, ETH_TOKEN_ADDRESS_32)
case 0 {
receiver := caller()
}
default {
receiver := address()
}
}
// Call pair.swap()
mstore(0xB00, UNISWAP_PAIR_SWAP_CALL_SELECTOR_32)
switch pairOrder
case 0 {
mstore(0xB04, buyAmount)
mstore(0xB24, 0)
}
default {
mstore(0xB04, 0)
mstore(0xB24, buyAmount)
}
mstore(0xB44, receiver)
mstore(0xB64, 0x80)
mstore(0xB84, 0)
if iszero(call(gas(), pair, 0, 0xB00, 0xA4, 0, 0)) {
bubbleRevert()
}
} // End for-loop.
// If buying ETH, unwrap the WETH first
if eq(buyToken, ETH_TOKEN_ADDRESS_32) {
// Call `WETH.withdraw(buyAmount)`
mstore(0xB00, WETH_WITHDRAW_CALL_SELECTOR_32)
mstore(0xB04, buyAmount)
if iszero(call(gas(), mload(0xA40), 0, 0xB00, 0x24, 0x00, 0x0)) {
bubbleRevert()
}
// Transfer ETH to the caller.
if iszero(call(gas(), caller(), buyAmount, 0xB00, 0x0, 0x00, 0x0)) {
bubbleRevert()
}
}
// Functions ///////////////////////////////////////////////////////
// Load a token address from the `tokens` calldata argument.
function loadTokenAddress(idx) -> addr {
addr := and(ADDRESS_MASK, calldataload(add(mload(0xA00), mul(idx, 0x20))))
}
// Convert ETH pseudo-token addresses to WETH.
function normalizeToken(token) -> normalized {
normalized := token
// Translate ETH pseudo-tokens to WETH.
if eq(token, ETH_TOKEN_ADDRESS_32) {
normalized := mload(0xA40)
}
}
// Compute the address of the UniswapV2Pair contract given two
// tokens.
function computePairAddress(tokenA, tokenB) -> pair {
// Convert ETH pseudo-token addresses to WETH.
tokenA := normalizeToken(tokenA)
tokenB := normalizeToken(tokenB)
// There is one contract for every combination of tokens,
// which is deployed using CREATE2.
// The derivation of this address is given by:
// address(keccak256(abi.encodePacked(
// bytes(0xFF),
// address(UNISWAP_FACTORY_ADDRESS),
// keccak256(abi.encodePacked(
// tokenA < tokenB ? tokenA : tokenB,
// tokenA < tokenB ? tokenB : tokenA,
// )),
// bytes32(UNISWAP_PAIR_INIT_CODE_HASH),
// )));
// Compute the salt (the hash of the sorted tokens).
// Tokens are written in reverse memory order to packed encode
// them as two 20-byte values in a 40-byte chunk of memory
// starting at 0xB0C.
switch lt(tokenA, tokenB)
case 0 {
mstore(0xB14, tokenA)
mstore(0xB00, tokenB)
}
default {
mstore(0xB14, tokenB)
mstore(0xB00, tokenA)
}
let salt := keccak256(0xB0C, 0x28)
// Compute the pair address by hashing all the components together.
switch mload(0xA20) // isSushi
case 0 {
mstore(0xB00, FF_UNISWAP_FACTORY)
mstore(0xB15, salt)
mstore(0xB35, UNISWAP_PAIR_INIT_CODE_HASH)
}
default {
mstore(0xB00, FF_SUSHISWAP_FACTORY)
mstore(0xB15, salt)
mstore(0xB35, SUSHISWAP_PAIR_INIT_CODE_HASH)
}
pair := and(ADDRESS_MASK, keccak256(0xB00, 0x55))
}
// Revert with the return data from the most recent call.
function bubbleRevert() {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
// Revert if we bought too little.
// TODO: replace with rich revert?
require(buyAmount >= minBuyAmount, "UniswapFeature/UnderBought");
}
}

View File

@ -81,6 +81,6 @@ abstract contract FixinCommon {
pure pure
returns (uint256 encodedVersion) returns (uint256 encodedVersion)
{ {
return (major << 64) | (minor << 32) | revision; return (uint256(major) << 64) | (uint256(minor) << 32) | uint256(revision);
} }
} }

View File

@ -29,6 +29,8 @@ library LibERC20Transformer {
/// @dev ETH pseudo-token address. /// @dev ETH pseudo-token address.
address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @dev ETH pseudo-token.
IERC20TokenV06 constant internal ETH_TOKEN = IERC20TokenV06(ETH_TOKEN_ADDRESS);
/// @dev Return value indicating success in `IERC20Transformer.transform()`. /// @dev Return value indicating success in `IERC20Transformer.transform()`.
/// This is just `keccak256('TRANSFORMER_SUCCESS')`. /// This is just `keccak256('TRANSFORMER_SUCCESS')`.
bytes4 constant internal TRANSFORMER_SUCCESS = 0x13c9929e; bytes4 constant internal TRANSFORMER_SUCCESS = 0x13c9929e;

View File

@ -41,7 +41,7 @@
"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",
"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|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|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|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"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -32,6 +32,7 @@ import * as ISimpleFunctionRegistryFeature from '../test/generated-artifacts/ISi
import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json';
import * as ITokenSpenderFeature from '../test/generated-artifacts/ITokenSpenderFeature.json'; import * as ITokenSpenderFeature from '../test/generated-artifacts/ITokenSpenderFeature.json';
import * as ITransformERC20Feature from '../test/generated-artifacts/ITransformERC20Feature.json'; import * as ITransformERC20Feature from '../test/generated-artifacts/ITransformERC20Feature.json';
import * as IUniswapFeature from '../test/generated-artifacts/IUniswapFeature.json';
import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json'; 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';
@ -96,6 +97,7 @@ import * as TokenSpenderFeature from '../test/generated-artifacts/TokenSpenderFe
import * as Transformer from '../test/generated-artifacts/Transformer.json'; import * as Transformer from '../test/generated-artifacts/Transformer.json';
import * as TransformERC20Feature from '../test/generated-artifacts/TransformERC20Feature.json'; import * as TransformERC20Feature from '../test/generated-artifacts/TransformERC20Feature.json';
import * as TransformerDeployer from '../test/generated-artifacts/TransformerDeployer.json'; import * as TransformerDeployer from '../test/generated-artifacts/TransformerDeployer.json';
import * as UniswapFeature from '../test/generated-artifacts/UniswapFeature.json';
import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json'; import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json';
import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json';
export const artifacts = { export const artifacts = {
@ -124,12 +126,14 @@ export const artifacts = {
ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact, ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact,
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature 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,
SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact,
TokenSpenderFeature: TokenSpenderFeature as ContractArtifact, TokenSpenderFeature: TokenSpenderFeature as ContractArtifact,
TransformERC20Feature: TransformERC20Feature as ContractArtifact, TransformERC20Feature: TransformERC20Feature as ContractArtifact,
UniswapFeature: UniswapFeature as ContractArtifact,
LibSignedCallData: LibSignedCallData as ContractArtifact, LibSignedCallData: LibSignedCallData as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact,
FixinEIP712: FixinEIP712 as ContractArtifact, FixinEIP712: FixinEIP712 as ContractArtifact,

View File

@ -29,6 +29,7 @@ export * from '../test/generated-wrappers/i_simple_function_registry_feature';
export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature';
export * from '../test/generated-wrappers/i_token_spender_feature'; export * from '../test/generated-wrappers/i_token_spender_feature';
export * from '../test/generated-wrappers/i_transform_erc20_feature'; export * from '../test/generated-wrappers/i_transform_erc20_feature';
export * from '../test/generated-wrappers/i_uniswap_feature';
export * from '../test/generated-wrappers/i_zero_ex'; export * from '../test/generated-wrappers/i_zero_ex';
export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/initial_migration';
export * from '../test/generated-wrappers/lib_bootstrap'; export * from '../test/generated-wrappers/lib_bootstrap';
@ -94,5 +95,6 @@ export * from '../test/generated-wrappers/token_spender_feature';
export * from '../test/generated-wrappers/transform_erc20_feature'; export * from '../test/generated-wrappers/transform_erc20_feature';
export * from '../test/generated-wrappers/transformer'; export * from '../test/generated-wrappers/transformer';
export * from '../test/generated-wrappers/transformer_deployer'; export * from '../test/generated-wrappers/transformer_deployer';
export * from '../test/generated-wrappers/uniswap_feature';
export * from '../test/generated-wrappers/weth_transformer'; export * from '../test/generated-wrappers/weth_transformer';
export * from '../test/generated-wrappers/zero_ex'; export * from '../test/generated-wrappers/zero_ex';

View File

@ -52,6 +52,7 @@
"test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json",
"test/generated-artifacts/ITokenSpenderFeature.json", "test/generated-artifacts/ITokenSpenderFeature.json",
"test/generated-artifacts/ITransformERC20Feature.json", "test/generated-artifacts/ITransformERC20Feature.json",
"test/generated-artifacts/IUniswapFeature.json",
"test/generated-artifacts/IZeroEx.json", "test/generated-artifacts/IZeroEx.json",
"test/generated-artifacts/InitialMigration.json", "test/generated-artifacts/InitialMigration.json",
"test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibBootstrap.json",
@ -117,6 +118,7 @@
"test/generated-artifacts/TransformERC20Feature.json", "test/generated-artifacts/TransformERC20Feature.json",
"test/generated-artifacts/Transformer.json", "test/generated-artifacts/Transformer.json",
"test/generated-artifacts/TransformerDeployer.json", "test/generated-artifacts/TransformerDeployer.json",
"test/generated-artifacts/UniswapFeature.json",
"test/generated-artifacts/WethTransformer.json", "test/generated-artifacts/WethTransformer.json",
"test/generated-artifacts/ZeroEx.json" "test/generated-artifacts/ZeroEx.json"
], ],

View File

@ -109,6 +109,14 @@
{ {
"note": "Added `SushiSwap`", "note": "Added `SushiSwap`",
"pr": 2698 "pr": 2698
},
{
"note": "Add uniswap VIP support",
"pr": 2703
},
{
"note": "Add `includedSources` support",
"pr": 2703
} }
] ]
}, },

View File

@ -71,6 +71,7 @@ const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts
sellTokenFeeAmount: ZERO_AMOUNT, sellTokenFeeAmount: ZERO_AMOUNT,
}, },
refundReceiver: NULL_ADDRESS, refundReceiver: NULL_ADDRESS,
isMetaTransaction: false,
}; };
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS; const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;

View File

@ -27,11 +27,12 @@ import {
SwapQuoteGetOutputOpts, SwapQuoteGetOutputOpts,
} from '../types'; } from '../types';
import { assert } from '../utils/assert'; import { assert } from '../utils/assert';
import { ERC20BridgeSource, UniswapV2FillData } from '../utils/market_operation_utils/types';
import { getTokenFromAssetData } from '../utils/utils'; import { getTokenFromAssetData } from '../utils/utils';
// tslint:disable-next-line:custom-no-magic-numbers // tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
const { NULL_ADDRESS } = constants; const { NULL_ADDRESS, ZERO_AMOUNT } = constants;
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider; public readonly provider: ZeroExProvider;
@ -82,16 +83,44 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
opts: Partial<SwapQuoteGetOutputOpts> = {}, opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> { ): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
// tslint:disable-next-line:no-object-literal-type-assertion const optsWithDefaults: ExchangeProxyContractOpts = {
const { refundReceiver, affiliateFee, isFromETH, isToETH } = {
...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS, ...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
...opts.extensionContractOpts, ...opts.extensionContractOpts,
} as ExchangeProxyContractOpts; };
// tslint:disable-next-line:no-object-literal-type-assertion
const { refundReceiver, affiliateFee, isFromETH, isToETH } = optsWithDefaults;
const sellToken = getTokenFromAssetData(quote.takerAssetData); const sellToken = getTokenFromAssetData(quote.takerAssetData);
const buyToken = getTokenFromAssetData(quote.makerAssetData); const buyToken = getTokenFromAssetData(quote.makerAssetData);
const sellAmount = quote.worstCaseQuoteInfo.totalTakerAssetAmount; const sellAmount = quote.worstCaseQuoteInfo.totalTakerAssetAmount;
// VIP routes.
if (isDirectUniswapCompatible(quote, optsWithDefaults)) {
const source = quote.orders[0].fills[0].source;
const fillData = quote.orders[0].fills[0].fillData as UniswapV2FillData;
return {
calldataHexString: this._exchangeProxy
.sellToUniswap(
fillData.tokenAddressPath.map((a, i) => {
if (i === 0 && isFromETH) {
return ETH_TOKEN_ADDRESS;
}
if (i === fillData.tokenAddressPath.length - 1 && isToETH) {
return ETH_TOKEN_ADDRESS;
}
return a;
}),
sellAmount,
quote.worstCaseQuoteInfo.makerAssetAmount,
source === ERC20BridgeSource.SushiSwap,
)
.getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget,
};
}
// Build up the transforms. // Build up the transforms.
const transforms = []; const transforms = [];
if (isFromETH) { if (isFromETH) {
@ -232,3 +261,29 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy; return quote.type === MarketOperation.Buy;
} }
function isDirectUniswapCompatible(quote: SwapQuote, opts: ExchangeProxyContractOpts): boolean {
// Must not be a mtx.
if (opts.isMetaTransaction) {
return false;
}
// Must not have an affiliate fee.
if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) {
return false;
}
// Must be a single order.
if (quote.orders.length !== 1) {
return false;
}
const order = quote.orders[0];
// With a single underlying fill/source.
if (order.fills.length !== 1) {
return false;
}
const fill = order.fills[0];
// And that fill must be uniswap v2 or sushiswap.
if (![ERC20BridgeSource.UniswapV2, ERC20BridgeSource.SushiSwap].includes(fill.source)) {
return false;
}
return true;
}

View File

@ -27,6 +27,7 @@ import { calculateLiquidity } from './utils/calculate_liquidity';
import { MarketOperationUtils } from './utils/market_operation_utils'; import { MarketOperationUtils } from './utils/market_operation_utils';
import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders';
import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { SourceFilters } from './utils/market_operation_utils/source_filters';
import { import {
ERC20BridgeSource, ERC20BridgeSource,
MarketDepth, MarketDepth,
@ -421,13 +422,13 @@ export class SwapQuoter {
assert.isString('takerTokenAddress', takerTokenAddress); assert.isString('takerTokenAddress', takerTokenAddress);
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
let [sellOrders, buyOrders] = const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources);
options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native) let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native)
? [[], []] ? [[], []]
: await Promise.all([ : await Promise.all([
this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), this.orderbook.getOrdersAsync(makerAssetData, takerAssetData),
this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), this.orderbook.getOrdersAsync(takerAssetData, makerAssetData),
]); ]);
if (!sellOrders || sellOrders.length === 0) { if (!sellOrders || sellOrders.length === 0) {
sellOrders = [ sellOrders = [
{ {
@ -652,12 +653,14 @@ export class SwapQuoter {
gasPrice = await this.getGasPriceEstimationOrThrowAsync(); gasPrice = await this.getGasPriceEstimationOrThrowAsync();
} }
const sourceFilters = new SourceFilters([], opts.excludedSources, opts.includedSources);
// If RFQT is enabled and `nativeExclusivelyRFQT` is set, then `ERC20BridgeSource.Native` should // If RFQT is enabled and `nativeExclusivelyRFQT` is set, then `ERC20BridgeSource.Native` should
// never be excluded. // never be excluded.
if ( if (
opts.rfqt && opts.rfqt &&
opts.rfqt.nativeExclusivelyRFQT === true && opts.rfqt.nativeExclusivelyRFQT === true &&
opts.excludedSources.includes(ERC20BridgeSource.Native) !sourceFilters.isAllowed(ERC20BridgeSource.Native)
) { ) {
throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQT" is set'); throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQT" is set');
} }
@ -666,7 +669,7 @@ export class SwapQuoter {
const orderBatchPromises: Array<Promise<SignedOrder[]>> = []; const orderBatchPromises: Array<Promise<SignedOrder[]>> = [];
const skipOpenOrderbook = const skipOpenOrderbook =
opts.excludedSources.includes(ERC20BridgeSource.Native) || !sourceFilters.isAllowed(ERC20BridgeSource.Native) ||
(opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true); (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true);
if (!skipOpenOrderbook) { if (!skipOpenOrderbook) {
orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book
@ -685,7 +688,7 @@ export class SwapQuoter {
opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote
opts.rfqt.apiKey && opts.rfqt.apiKey &&
this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided
!opts.excludedSources.includes(ERC20BridgeSource.Native) // Native liquidity is not excluded sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded
) { ) {
if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address'); throw new Error('RFQ-T requests must specify a taker address');

View File

@ -160,6 +160,7 @@ export interface ExchangeProxyContractOpts {
isToETH: boolean; isToETH: boolean;
affiliateFee: AffiliateFee; affiliateFee: AffiliateFee;
refundReceiver: string | ExchangeProxyRefundReceiver; refundReceiver: string | ExchangeProxyRefundReceiver;
isMetaTransaction: boolean;
} }
export interface GetExtensionContractTypeOpts { export interface GetExtensionContractTypeOpts {

View File

@ -2,8 +2,6 @@ import { BigNumber } from '@0x/utils';
import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Decimal } from 'decimal.js'; import { Decimal } from 'decimal.js';
import { ERC20BridgeSource } from './types';
// tslint:disable:boolean-naming // tslint:disable:boolean-naming
export interface BalancerPool { export interface BalancerPool {
@ -67,10 +65,10 @@ export class BalancerPoolsCache {
public howToSampleBalancer( public howToSampleBalancer(
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
excludedSources: ERC20BridgeSource[], isAllowedSource: boolean,
): { onChain: boolean; offChain: boolean } { ): { onChain: boolean; offChain: boolean } {
// If Balancer is excluded as a source, do not sample. // If Balancer is excluded as a source, do not sample.
if (excludedSources.includes(ERC20BridgeSource.Balancer)) { if (!isAllowedSource) {
return { onChain: false, offChain: false }; return { onChain: false, offChain: false };
} }
const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS);

View File

@ -1,5 +1,6 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
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
@ -7,7 +8,8 @@ import { CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, GetMarketOrdersOp
/** /**
* Valid sources for market sell. * Valid sources for market sell.
*/ */
export const SELL_SOURCES = [ export const SELL_SOURCE_FILTER = new SourceFilters([
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
@ -20,29 +22,36 @@ export const SELL_SOURCES = [
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Swerve, ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
]; ERC20BridgeSource.MultiHop,
]);
/** /**
* Valid sources for market buy. * Valid sources for market buy.
*/ */
export const BUY_SOURCES = [ export const BUY_SOURCE_FILTER = new SourceFilters(
ERC20BridgeSource.Uniswap, [
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Kyber, ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Curve, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Balancer, ERC20BridgeSource.Kyber,
// ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes ERC20BridgeSource.Curve,
ERC20BridgeSource.MStable, ERC20BridgeSource.Balancer,
ERC20BridgeSource.Mooniswap, // ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes
ERC20BridgeSource.Swerve, ERC20BridgeSource.MStable,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.Mooniswap,
]; ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop,
],
[ERC20BridgeSource.MultiBridge],
);
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
// tslint:disable-next-line: custom-no-magic-numbers // tslint:disable-next-line: custom-no-magic-numbers
runLimit: 2 ** 15, runLimit: 2 ** 15,
excludedSources: [], excludedSources: [],
includedSources: [],
bridgeSlippage: 0.005, bridgeSlippage: 0.005,
maxFallbackSlippage: 0.05, maxFallbackSlippage: 0.05,
numSamples: 13, numSamples: 13,

View File

@ -6,15 +6,14 @@ import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { QuoteRequestor } from '../quote_requestor'; import { QuoteRequestor } from '../quote_requestor';
import { difference } from '../utils';
import { generateQuoteReport } from './../quote_report_generator'; import { generateQuoteReport } from './../quote_report_generator';
import { import {
BUY_SOURCES, BUY_SOURCE_FILTER,
DEFAULT_GET_MARKET_ORDERS_OPTS, DEFAULT_GET_MARKET_ORDERS_OPTS,
FEE_QUOTE_SOURCES, FEE_QUOTE_SOURCES,
ONE_ETHER, ONE_ETHER,
SELL_SOURCES, SELL_SOURCE_FILTER,
ZERO_AMOUNT, ZERO_AMOUNT,
} from './constants'; } from './constants';
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
@ -28,6 +27,7 @@ import {
} from './orders'; } from './orders';
import { findOptimalPathAsync } from './path_optimizer'; import { findOptimalPathAsync } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters';
import { import {
AggregationError, AggregationError,
DexSample, DexSample,
@ -58,8 +58,7 @@ export async function getRfqtIndicativeQuotesAsync(
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
opts: Partial<GetMarketOrdersOpts>, opts: Partial<GetMarketOrdersOpts>,
): Promise<RFQTIndicativeQuote[]> { ): Promise<RFQTIndicativeQuote[]> {
const hasExcludedNativeLiquidity = opts.excludedSources && opts.excludedSources.includes(ERC20BridgeSource.Native); if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) {
if (!hasExcludedNativeLiquidity && opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) {
return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
@ -75,6 +74,9 @@ export async function getRfqtIndicativeQuotesAsync(
export class MarketOperationUtils { export class MarketOperationUtils {
private readonly _wethAddress: string; private readonly _wethAddress: string;
private readonly _multiBridge: string; private readonly _multiBridge: string;
private readonly _sellSources: SourceFilters;
private readonly _buySources: SourceFilters;
private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES);
constructor( constructor(
private readonly _sampler: DexOrderSampler, private readonly _sampler: DexOrderSampler,
@ -85,6 +87,15 @@ export class MarketOperationUtils {
) { ) {
this._wethAddress = contractAddresses.etherToken.toLowerCase(); this._wethAddress = contractAddresses.etherToken.toLowerCase();
this._multiBridge = contractAddresses.multiBridge.toLowerCase(); this._multiBridge = contractAddresses.multiBridge.toLowerCase();
const optionalQuoteSources = [];
if (this._liquidityProviderRegistry !== NULL_ADDRESS) {
optionalQuoteSources.push(ERC20BridgeSource.LiquidityProvider);
}
if (this._multiBridge !== NULL_ADDRESS) {
optionalQuoteSources.push(ERC20BridgeSource.MultiBridge);
}
this._buySources = BUY_SOURCE_FILTER.validate(optionalQuoteSources);
this._sellSources = SELL_SOURCE_FILTER.validate(optionalQuoteSources);
} }
/** /**
@ -105,11 +116,18 @@ export class MarketOperationUtils {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase); const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const feeSourceFilters = this._feeSources.merge(requestFilters);
const quoteSourceFilters = this._sellSources.merge(requestFilters);
const { const {
onChain: sampleBalancerOnChain, onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain, offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources); } = this._sampler.balancerPoolsCache.howToSampleBalancer(
takerToken,
makerToken,
quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer),
);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
@ -117,7 +135,7 @@ export class MarketOperationUtils {
this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> maker token price. // Get ETH -> maker token price.
this._sampler.getMedianSellRate( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), feeSourceFilters.sources,
makerToken, makerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
@ -127,7 +145,7 @@ export class MarketOperationUtils {
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
this._sampler.getMedianSellRate( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), feeSourceFilters.sources,
takerToken, takerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
@ -137,10 +155,7 @@ export class MarketOperationUtils {
), ),
// Get sell quotes for taker -> maker. // Get sell quotes for taker -> maker.
this._sampler.getSellQuotes( this._sampler.getSellQuotes(
difference( quoteSourceFilters.exclude(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer).sources,
SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
),
makerToken, makerToken,
takerToken, takerToken,
sampleAmounts, sampleAmounts,
@ -148,37 +163,34 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop) this._sampler.getTwoHopSellQuotes(
? DexOrderSampler.constant([]) quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
: this._sampler.getTwoHopSellQuotes( makerToken,
difference( takerToken,
SELL_SOURCES.concat(this._optionalSources()), takerAmount,
_opts.excludedSources.concat(ERC20BridgeSource.MultiBridge), this._tokenAdjacencyGraph,
), this._wethAddress,
makerToken, this._liquidityProviderRegistry,
takerToken, ),
takerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
); );
const rfqtPromise = getRfqtIndicativeQuotesAsync( const rfqtPromise = quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
nativeOrders[0].makerAssetData, ? getRfqtIndicativeQuotesAsync(
nativeOrders[0].takerAssetData, nativeOrders[0].makerAssetData,
MarketOperation.Sell, nativeOrders[0].takerAssetData,
takerAmount, MarketOperation.Sell,
_opts, takerAmount,
); _opts,
)
: Promise.resolve([]);
const offChainBalancerPromise = sampleBalancerOffChain const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) ? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]); : Promise.resolve([]);
const offChainBancorPromise = _opts.excludedSources.includes(ERC20BridgeSource.Bancor) const offChainBancorPromise = quoteSourceFilters.isAllowed(ERC20BridgeSource.Bancor)
? Promise.resolve([]) ? this._sampler.getBancorSellQuotesOffChainAsync(makerToken, takerToken, [takerAmount])
: this._sampler.getBancorSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts); : Promise.resolve([]);
const [ const [
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
@ -220,11 +232,18 @@ export class MarketOperationUtils {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const feeSourceFilters = this._feeSources.merge(requestFilters);
const quoteSourceFilters = this._buySources.merge(requestFilters);
const { const {
onChain: sampleBalancerOnChain, onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain, offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources); } = this._sampler.balancerPoolsCache.howToSampleBalancer(
takerToken,
makerToken,
quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer),
);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
@ -232,7 +251,7 @@ export class MarketOperationUtils {
this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> makerToken token price. // Get ETH -> makerToken token price.
this._sampler.getMedianSellRate( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), feeSourceFilters.sources,
makerToken, makerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
@ -242,7 +261,7 @@ export class MarketOperationUtils {
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
this._sampler.getMedianSellRate( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), feeSourceFilters.sources,
takerToken, takerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
@ -252,49 +271,38 @@ export class MarketOperationUtils {
), ),
// Get buy quotes for taker -> maker. // Get buy quotes for taker -> maker.
this._sampler.getBuyQuotes( this._sampler.getBuyQuotes(
difference( quoteSourceFilters.exclude(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer).sources,
BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [],
),
_opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
),
makerToken, makerToken,
takerToken, takerToken,
sampleAmounts, sampleAmounts,
this._wethAddress, this._wethAddress,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
), ),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop) this._sampler.getTwoHopBuyQuotes(
? DexOrderSampler.constant([]) quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
: this._sampler.getTwoHopBuyQuotes( makerToken,
difference( takerToken,
BUY_SOURCES.concat( makerAmount,
this._liquidityProviderRegistry !== NULL_ADDRESS this._tokenAdjacencyGraph,
? [ERC20BridgeSource.LiquidityProvider] this._wethAddress,
: [], this._liquidityProviderRegistry,
), ),
_opts.excludedSources,
),
makerToken,
takerToken,
makerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
); );
const rfqtPromise = quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
? getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
MarketOperation.Buy,
makerAmount,
_opts,
)
: Promise.resolve([]);
const offChainBalancerPromise = sampleBalancerOffChain const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]); : Promise.resolve([]);
const rfqtPromise = getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
MarketOperation.Buy,
makerAmount,
_opts,
);
const [ const [
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
@ -394,14 +402,17 @@ export class MarketOperationUtils {
} }
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const sources = difference(BUY_SOURCES, _opts.excludedSources.concat(ERC20BridgeSource.Balancer)); const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const feeSourceFilters = this._feeSources.merge(requestFilters);
const quoteSourceFilters = this._buySources.merge(requestFilters);
const ops = [ const ops = [
...batchNativeOrders.map(orders => ...batchNativeOrders.map(orders =>
this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange), this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange),
), ),
...batchNativeOrders.map(orders => ...batchNativeOrders.map(orders =>
this._sampler.getMedianSellRate( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), feeSourceFilters.sources,
getNativeOrderTokens(orders[0])[1], getNativeOrderTokens(orders[0])[1],
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
@ -410,7 +421,7 @@ export class MarketOperationUtils {
), ),
...batchNativeOrders.map((orders, i) => ...batchNativeOrders.map((orders, i) =>
this._sampler.getBuyQuotes( this._sampler.getBuyQuotes(
sources, quoteSourceFilters.sources,
getNativeOrderTokens(orders[0])[0], getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[1], getNativeOrderTokens(orders[0])[1],
[makerAmounts[i]], [makerAmounts[i]],
@ -593,12 +604,6 @@ export class MarketOperationUtils {
: undefined; : undefined;
return { optimizedOrders, quoteReport, isTwoHop: false }; return { optimizedOrders, quoteReport, isTwoHop: false };
} }
private _optionalSources(): ERC20BridgeSource[] {
return (this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : []).concat(
this._multiBridge !== NULL_ADDRESS ? [ERC20BridgeSource.MultiBridge] : [],
);
}
} }
// tslint:disable: max-file-line-count // tslint:disable: max-file-line-count

View File

@ -13,6 +13,7 @@ import { getKyberReserveIdsForPair } from './kyber_utils';
import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import { getIntermediateTokens } from './multihop_utils'; import { getIntermediateTokens } from './multihop_utils';
import { SamplerContractOperation } from './sampler_contract_operation'; import { SamplerContractOperation } from './sampler_contract_operation';
import { SourceFilters } from './source_filters';
import { import {
BalancerFillData, BalancerFillData,
BancorFillData, BancorFillData,
@ -35,6 +36,19 @@ import {
UniswapV2FillData, UniswapV2FillData,
} from './types'; } from './types';
/**
* Source filters for `getTwoHopBuyQuotes()` and `getTwoHopSellQuotes()`.
*/
export const TWO_HOP_SOURCE_FILTERS = SourceFilters.all().exclude([
ERC20BridgeSource.MultiHop,
ERC20BridgeSource.MultiBridge,
ERC20BridgeSource.Native,
]);
/**
* Source filters for `getSellQuotes()` and `getBuyQuotes()`.
*/
export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]);
// tslint:disable:no-inferred-empty-object-type no-unbound-method // tslint:disable:no-inferred-empty-object-type no-unbound-method
/** /**
@ -553,10 +567,14 @@ export class SamplerOperations {
wethAddress: string, wethAddress: string,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
): BatchedOperation<Array<DexSample<MultiHopFillData>>> { ): BatchedOperation<Array<DexSample<MultiHopFillData>>> {
const _sources = TWO_HOP_SOURCE_FILTERS.getAllowed(sources);
if (_sources.length === 0) {
return SamplerOperations.constant([]);
}
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress); const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress);
const subOps = intermediateTokens.map(intermediateToken => { const subOps = intermediateTokens.map(intermediateToken => {
const firstHopOps = this._getSellQuoteOperations( const firstHopOps = this._getSellQuoteOperations(
sources, _sources,
intermediateToken, intermediateToken,
takerToken, takerToken,
[ZERO_AMOUNT], [ZERO_AMOUNT],
@ -564,7 +582,7 @@ export class SamplerOperations {
liquidityProviderRegistryAddress, liquidityProviderRegistryAddress,
); );
const secondHopOps = this._getSellQuoteOperations( const secondHopOps = this._getSellQuoteOperations(
sources, _sources,
makerToken, makerToken,
intermediateToken, intermediateToken,
[ZERO_AMOUNT], [ZERO_AMOUNT],
@ -624,10 +642,14 @@ export class SamplerOperations {
wethAddress: string, wethAddress: string,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
): BatchedOperation<Array<DexSample<MultiHopFillData>>> { ): BatchedOperation<Array<DexSample<MultiHopFillData>>> {
const _sources = TWO_HOP_SOURCE_FILTERS.getAllowed(sources);
if (_sources.length === 0) {
return SamplerOperations.constant([]);
}
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress); const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress);
const subOps = intermediateTokens.map(intermediateToken => { const subOps = intermediateTokens.map(intermediateToken => {
const firstHopOps = this._getBuyQuoteOperations( const firstHopOps = this._getBuyQuoteOperations(
sources, _sources,
intermediateToken, intermediateToken,
takerToken, takerToken,
[new BigNumber(0)], [new BigNumber(0)],
@ -635,7 +657,7 @@ export class SamplerOperations {
liquidityProviderRegistryAddress, liquidityProviderRegistryAddress,
); );
const secondHopOps = this._getBuyQuoteOperations( const secondHopOps = this._getBuyQuoteOperations(
sources, _sources,
makerToken, makerToken,
intermediateToken, intermediateToken,
[new BigNumber(0)], [new BigNumber(0)],
@ -776,8 +798,9 @@ export class SamplerOperations {
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string, multiBridgeAddress?: string,
): BatchedOperation<DexSample[][]> { ): BatchedOperation<DexSample[][]> {
const _sources = BATCH_SOURCE_FILTERS.getAllowed(sources);
const subOps = this._getSellQuoteOperations( const subOps = this._getSellQuoteOperations(
sources, _sources,
makerToken, makerToken,
takerToken, takerToken,
takerFillAmounts, takerFillAmounts,
@ -816,8 +839,9 @@ export class SamplerOperations {
wethAddress: string, wethAddress: string,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
): BatchedOperation<DexSample[][]> { ): BatchedOperation<DexSample[][]> {
const _sources = BATCH_SOURCE_FILTERS.getAllowed(sources);
const subOps = this._getBuyQuoteOperations( const subOps = this._getBuyQuoteOperations(
sources, _sources,
makerToken, makerToken,
takerToken, takerToken,
makerFillAmounts, makerFillAmounts,

View File

@ -0,0 +1,95 @@
import * as _ from 'lodash';
import { ERC20BridgeSource } from './types';
export class SourceFilters {
// All valid sources.
private readonly _validSources: ERC20BridgeSource[];
// Sources in `_validSources` that are not allowed.
private readonly _excludedSources: ERC20BridgeSource[];
// Sources in `_validSources` that are only allowed.
private readonly _includedSources: ERC20BridgeSource[];
public static all(): SourceFilters {
return new SourceFilters(Object.values(ERC20BridgeSource));
}
constructor(
validSources: ERC20BridgeSource[] = [],
excludedSources: ERC20BridgeSource[] = [],
includedSources: ERC20BridgeSource[] = [],
) {
this._validSources = _.uniq(validSources);
this._excludedSources = _.uniq(excludedSources);
this._includedSources = _.uniq(includedSources);
}
public isAllowed(source: ERC20BridgeSource): boolean {
// Must be in list of valid sources.
if (this._validSources.length > 0 && !this._validSources.includes(source)) {
return false;
}
// Must not be excluded.
if (this._excludedSources.includes(source)) {
return false;
}
// If we have an inclusion list, it must be in that list.
if (this._includedSources.length > 0 && !this._includedSources.includes(source)) {
return false;
}
return true;
}
public areAnyAllowed(sources: ERC20BridgeSource[]): boolean {
return sources.some(s => this.isAllowed(s));
}
public areAllAllowed(sources: ERC20BridgeSource[]): boolean {
return sources.every(s => this.isAllowed(s));
}
public getAllowed(sources: ERC20BridgeSource[] = []): ERC20BridgeSource[] {
return sources.filter(s => this.isAllowed(s));
}
public get sources(): ERC20BridgeSource[] {
return this._validSources.filter(s => this.isAllowed(s));
}
public exclude(sources: ERC20BridgeSource | ERC20BridgeSource[]): SourceFilters {
return new SourceFilters(
this._validSources,
[...this._excludedSources, ...(Array.isArray(sources) ? sources : [sources])],
this._includedSources,
);
}
public validate(sources: ERC20BridgeSource | ERC20BridgeSource[]): SourceFilters {
return new SourceFilters(
[...this._validSources, ...(Array.isArray(sources) ? sources : [sources])],
this._excludedSources,
this._includedSources,
);
}
public include(sources: ERC20BridgeSource | ERC20BridgeSource[]): SourceFilters {
return new SourceFilters(this._validSources, this._excludedSources, [
...this._includedSources,
...(Array.isArray(sources) ? sources : [sources]),
]);
}
public merge(other: SourceFilters): SourceFilters {
let validSources = this._validSources;
if (validSources.length === 0) {
validSources = other._validSources;
} else if (other._validSources.length !== 0) {
validSources = validSources.filter(s => other._validSources.includes(s));
}
return new SourceFilters(
validSources,
[...this._excludedSources, ...other._excludedSources],
[...this._includedSources, ...other._includedSources],
);
}
}

View File

@ -243,6 +243,11 @@ export interface GetMarketOrdersOpts {
* Liquidity sources to exclude. Default is none. * Liquidity sources to exclude. Default is none.
*/ */
excludedSources: ERC20BridgeSource[]; excludedSources: ERC20BridgeSource[];
/**
* Liquidity sources to include. Default is none, which allows all supported
* sources that aren't excluded by `excludedSources`.
*/
includedSources: ERC20BridgeSource[];
/** /**
* Complexity limit on the search algorithm, i.e., maximum number of * Complexity limit on the search algorithm, i.e., maximum number of
* nodes to visit. Default is 1024. * nodes to visit. Default is 1024.

View File

@ -107,13 +107,6 @@ export function isERC20EquivalentAssetData(assetData: AssetData): assetData is E
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData); return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
} }
/**
* Gets the difference between two sets.
*/
export function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1);
}
export function getTokenFromAssetData(assetData: string): string { export function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData); const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) { if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) {

View File

@ -18,10 +18,22 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils'; import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants'; import {
BUY_SOURCE_FILTER,
POSITIVE_INF,
SELL_SOURCE_FILTER,
ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants';
import { createFillPaths } from '../src/utils/market_operation_utils/fills'; import { createFillPaths } 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 { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import {
DexSample,
ERC20BridgeSource,
FillData,
NativeFillData,
OptimizedMarketOrder,
} from '../src/utils/market_operation_utils/types';
const MAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
@ -36,7 +48,10 @@ const DEFAULT_EXCLUDED = [
ERC20BridgeSource.Bancor, ERC20BridgeSource.Bancor,
ERC20BridgeSource.Swerve, ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop,
]; ];
const BUY_SOURCES = BUY_SOURCE_FILTER.sources;
const SELL_SOURCES = SELL_SOURCE_FILTER.sources;
// tslint:disable: custom-no-magic-numbers promise-function-async // tslint:disable: custom-no-magic-numbers promise-function-async
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
@ -167,7 +182,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
_wethAddress: string, _wethAddress: string,
) => { ) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s])); return BATCH_SOURCE_FILTERS.getAllowed(sources).map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
}; };
} }
@ -209,7 +224,9 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
_wethAddress: string, _wethAddress: string,
) => { ) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))); return BATCH_SOURCE_FILTERS.getAllowed(sources).map(s =>
createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))),
);
}; };
} }
@ -244,6 +261,27 @@ 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 {
@ -265,6 +303,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Mooniswap]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Mooniswap]: _.times(NUM_SAMPLES, () => 0),
[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),
}; };
const DEFAULT_RATES: RatesBySource = { const DEFAULT_RATES: RatesBySource = {
@ -307,6 +346,9 @@ describe('MarketOperationUtils tests', () => {
}, },
[ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() }, [ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() },
[ERC20BridgeSource.SushiSwap]: { tokenAddressPath: [] }, [ERC20BridgeSource.SushiSwap]: { tokenAddressPath: [] },
[ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() },
[ERC20BridgeSource.Native]: { order: createOrder() },
[ERC20BridgeSource.MultiHop]: {},
}; };
const DEFAULT_OPS = { const DEFAULT_OPS = {
@ -356,7 +398,7 @@ describe('MarketOperationUtils tests', () => {
const MOCK_SAMPLER = ({ const MOCK_SAMPLER = ({
async executeAsync(...ops: any[]): Promise<any[]> { async executeAsync(...ops: any[]): Promise<any[]> {
return ops; return MOCK_SAMPLER.executeBatchAsync(ops);
}, },
async executeBatchAsync(ops: any[]): Promise<any[]> { async executeBatchAsync(ops: any[]): Promise<any[]> {
return ops; return ops;
@ -377,33 +419,7 @@ describe('MarketOperationUtils tests', () => {
intentOnFilling: false, intentOnFilling: false,
}; };
it('returns an empty array if native liquidity is excluded from the salad', async () => { it('calls RFQT', async () => {
const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Strict);
const result = await getRfqtIndicativeQuotesAsync(
MAKER_ASSET_DATA,
TAKER_ASSET_DATA,
MarketOperation.Sell,
new BigNumber('100e18'),
{
rfqt: { quoteRequestor: requestor.object, ...partialRfqt },
excludedSources: [ERC20BridgeSource.Native],
},
);
expect(result.length).to.eql(0);
requestor.verify(
r =>
r.requestRfqtIndicativeQuotesAsync(
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
),
TypeMoq.Times.never(),
);
});
it('calls RFQT if Native source is not excluded', async () => {
const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose); const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose);
requestor requestor
.setup(r => .setup(r =>
@ -424,7 +440,6 @@ describe('MarketOperationUtils tests', () => {
new BigNumber('100e18'), new BigNumber('100e18'),
{ {
rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, rfqt: { quoteRequestor: requestor.object, ...partialRfqt },
excludedSources: [],
}, },
); );
requestor.verifyAll(); requestor.verifyAll();
@ -481,6 +496,10 @@ describe('MarketOperationUtils tests', () => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
getTwoHopSellQuotes: (...args: any[]) => {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
return DEFAULT_OPS.getTwoHopSellQuotes(...args);
},
getBalancerSellQuotesOffChainAsync: ( getBalancerSellQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -494,7 +513,7 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], excludedSources: [],
}); });
expect(sourcesPolled.sort()).to.deep.equals(SELL_SOURCES.slice().sort()); expect(_.uniq(sourcesPolled).sort()).to.deep.equals(SELL_SOURCES.slice().sort());
}); });
it('polls the liquidity provider when the registry is provided in the arguments', async () => { it('polls the liquidity provider when the registry is provided in the arguments', async () => {
@ -504,6 +523,13 @@ describe('MarketOperationUtils tests', () => {
); );
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: fn, getSellQuotes: fn,
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
args.sources.push(ERC20BridgeSource.MultiHop);
args.sources.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(..._args);
},
getBalancerSellQuotesOffChainAsync: ( getBalancerSellQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -524,20 +550,27 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], excludedSources: [],
}); });
expect(args.sources.sort()).to.deep.equals( expect(_.uniq(args.sources).sort()).to.deep.equals(
SELL_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(), SELL_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(),
); );
expect(args.liquidityProviderAddress).to.eql(registryAddress); expect(args.liquidityProviderAddress).to.eql(registryAddress);
}); });
it('does not poll DEXes in `excludedSources`', async () => { it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(...args);
},
getBalancerSellQuotesOffChainAsync: ( getBalancerSellQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -551,7 +584,39 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources, excludedSources,
}); });
expect(sourcesPolled.sort()).to.deep.equals(_.without(SELL_SOURCES, ...excludedSources).sort()); expect(_.uniq(sourcesPolled).sort()).to.deep.equals(_.without(SELL_SOURCES, ...excludedSources).sort());
});
it('only polls DEXes in `includedSources`', async () => {
const includedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(sources, ...args);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
includedSources,
});
expect(_.uniq(sourcesPolled).sort()).to.deep.equals(includedSources.sort());
}); });
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
@ -858,7 +923,7 @@ describe('MarketOperationUtils tests', () => {
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(3); expect(improvedOrders).to.be.length(3);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Uniswap], [ERC20BridgeSource.Uniswap],
[ERC20BridgeSource.Native], [ERC20BridgeSource.Native],
@ -910,6 +975,13 @@ describe('MarketOperationUtils tests', () => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: ( getBalancerBuyQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -923,7 +995,7 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], excludedSources: [],
}); });
expect(sourcesPolled.sort()).to.deep.equals(BUY_SOURCES.sort()); expect(_.uniq(sourcesPolled).sort()).to.deep.equals(BUY_SOURCES.sort());
}); });
it('polls the liquidity provider when the registry is provided in the arguments', async () => { it('polls the liquidity provider when the registry is provided in the arguments', async () => {
@ -933,6 +1005,13 @@ describe('MarketOperationUtils tests', () => {
); );
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: fn, getBuyQuotes: fn,
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
args.sources.push(ERC20BridgeSource.MultiHop);
args.sources.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: ( getBalancerBuyQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -953,20 +1032,27 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], excludedSources: [],
}); });
expect(args.sources.sort()).to.deep.eq( expect(_.uniq(args.sources).sort()).to.deep.eq(
BUY_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(), BUY_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(),
); );
expect(args.liquidityProviderAddress).to.eql(registryAddress); expect(args.liquidityProviderAddress).to.eql(registryAddress);
}); });
it('does not poll DEXes in `excludedSources`', async () => { it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: ( getBalancerBuyQuotesOffChainAsync: (
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -980,7 +1066,39 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources, excludedSources,
}); });
expect(sourcesPolled.sort()).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources).sort()); expect(_.uniq(sourcesPolled).sort()).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources).sort());
});
it('only polls DEXes in `includedSources`', async () => {
const includedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
includedSources,
});
expect(_.uniq(sourcesPolled).sort()).to.deep.eq(includedSources.sort());
}); });
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
@ -1198,7 +1316,7 @@ describe('MarketOperationUtils tests', () => {
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(2); expect(improvedOrders).to.be.length(2);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Native], [ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],

View File

@ -13,6 +13,10 @@
{ {
"note": "Remove `ERC20BridgeSampler` artifact", "note": "Remove `ERC20BridgeSampler` artifact",
"pr": 2647 "pr": 2647
},
{
"note": "Regenerate artifacts",
"pr": 2703
} }
] ]
}, },

View File

@ -85,7 +85,7 @@
{ "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" }, { "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" },
{ "internalType": "uint256", "name": "feeAmount", "type": "uint256" } { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }
], ],
"internalType": "struct IMetaTransactions.MetaTransactionData", "internalType": "struct IMetaTransactionsFeature.MetaTransactionData",
"name": "mtx", "name": "mtx",
"type": "tuple" "type": "tuple"
}, },
@ -122,14 +122,14 @@
{ "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" }, { "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" },
{ "internalType": "bytes", "name": "data", "type": "bytes" } { "internalType": "bytes", "name": "data", "type": "bytes" }
], ],
"internalType": "struct ITransformERC20.Transformation[]", "internalType": "struct ITransformERC20Feature.Transformation[]",
"name": "transformations", "name": "transformations",
"type": "tuple[]" "type": "tuple[]"
}, },
{ "internalType": "bytes32", "name": "callDataHash", "type": "bytes32" }, { "internalType": "bytes32", "name": "callDataHash", "type": "bytes32" },
{ "internalType": "bytes", "name": "callDataSignature", "type": "bytes" } { "internalType": "bytes", "name": "callDataSignature", "type": "bytes" }
], ],
"internalType": "struct ITransformERC20.TransformERC20Args", "internalType": "struct ITransformERC20Feature.TransformERC20Args",
"name": "args", "name": "args",
"type": "tuple" "type": "tuple"
} }
@ -154,7 +154,7 @@
{ "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" }, { "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" },
{ "internalType": "uint256", "name": "feeAmount", "type": "uint256" } { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }
], ],
"internalType": "struct IMetaTransactions.MetaTransactionData[]", "internalType": "struct IMetaTransactionsFeature.MetaTransactionData[]",
"name": "mtxs", "name": "mtxs",
"type": "tuple[]" "type": "tuple[]"
}, },
@ -187,7 +187,7 @@
{ "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" }, { "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" },
{ "internalType": "uint256", "name": "feeAmount", "type": "uint256" } { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }
], ],
"internalType": "struct IMetaTransactions.MetaTransactionData", "internalType": "struct IMetaTransactionsFeature.MetaTransactionData",
"name": "mtx", "name": "mtx",
"type": "tuple" "type": "tuple"
}, },
@ -237,7 +237,7 @@
{ "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" }, { "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" },
{ "internalType": "uint256", "name": "feeAmount", "type": "uint256" } { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }
], ],
"internalType": "struct IMetaTransactions.MetaTransactionData", "internalType": "struct IMetaTransactionsFeature.MetaTransactionData",
"name": "mtx", "name": "mtx",
"type": "tuple" "type": "tuple"
} }
@ -262,7 +262,7 @@
{ "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" }, { "internalType": "contract IERC20TokenV06", "name": "feeToken", "type": "address" },
{ "internalType": "uint256", "name": "feeAmount", "type": "uint256" } { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }
], ],
"internalType": "struct IMetaTransactions.MetaTransactionData", "internalType": "struct IMetaTransactionsFeature.MetaTransactionData",
"name": "mtx", "name": "mtx",
"type": "tuple" "type": "tuple"
} }
@ -366,6 +366,18 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{ "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" },
{ "internalType": "uint256", "name": "sellAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" },
{ "internalType": "bool", "name": "isSushi", "type": "bool" }
],
"name": "sellToUniswap",
"outputs": [{ "internalType": "uint256", "name": "buyAmount", "type": "uint256" }],
"stateMutability": "payable",
"type": "function"
},
{ {
"inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }], "inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }],
"name": "setQuoteSigner", "name": "setQuoteSigner",
@ -398,7 +410,7 @@
{ "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" }, { "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" },
{ "internalType": "bytes", "name": "data", "type": "bytes" } { "internalType": "bytes", "name": "data", "type": "bytes" }
], ],
"internalType": "struct ITransformERC20.Transformation[]", "internalType": "struct ITransformERC20Feature.Transformation[]",
"name": "transformations", "name": "transformations",
"type": "tuple[]" "type": "tuple[]"
} }
@ -552,6 +564,16 @@
"targetImpl": "The address of an older implementation of the function." "targetImpl": "The address of an older implementation of the function."
} }
}, },
"sellToUniswap(address[],uint256,uint256,bool)": {
"details": "Efficiently sell directly to uniswap/sushiswap.",
"params": {
"isSushi": "Use sushiswap if true.",
"minBuyAmount": "Minimum amount of `tokens[-1]` to buy.",
"sellAmount": "of `tokens[0]` Amount to sell.",
"tokens": "Sell path."
},
"returns": { "buyAmount": "Amount of `tokens[-1]` bought." }
},
"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

@ -13,6 +13,10 @@
{ {
"note": "Add `exchangeProxy` to `ContractWrappers` type.", "note": "Add `exchangeProxy` to `ContractWrappers` type.",
"pr": 2649 "pr": 2649
},
{
"note": "Regenerate wrappers",
"pr": 2703
} }
] ]
}, },

View File

@ -1000,6 +1000,35 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'nonpayable', stateMutability: 'nonpayable',
type: 'function', type: 'function',
}, },
{
inputs: [
{
name: 'tokens',
type: 'address[]',
},
{
name: 'sellAmount',
type: 'uint256',
},
{
name: 'minBuyAmount',
type: 'uint256',
},
{
name: 'isSushi',
type: 'bool',
},
],
name: 'sellToUniswap',
outputs: [
{
name: 'buyAmount',
type: 'uint256',
},
],
stateMutability: 'payable',
type: 'function',
},
{ {
inputs: [ inputs: [
{ {
@ -2418,6 +2447,68 @@ export class IZeroExContract extends BaseContract {
}, },
}; };
} }
/**
* Efficiently sell directly to uniswap/sushiswap.
* @param tokens Sell path.
* @param sellAmount of `tokens[0]` Amount to sell.
* @param minBuyAmount Minimum amount of `tokens[-1]` to buy.
* @param isSushi Use sushiswap if true.
*/
public sellToUniswap(
tokens: string[],
sellAmount: BigNumber,
minBuyAmount: BigNumber,
isSushi: boolean,
): ContractTxFunctionObj<BigNumber> {
const self = (this as any) as IZeroExContract;
assert.isArray('tokens', tokens);
assert.isBigNumber('sellAmount', sellAmount);
assert.isBigNumber('minBuyAmount', minBuyAmount);
assert.isBoolean('isSushi', isSushi);
const functionSignature = 'sellToUniswap(address[],uint256,uint256,bool)';
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, [tokens, sellAmount, minBuyAmount, isSushi]);
},
};
}
/** /**
* Replace the optional signer for `transformERC20()` calldata. * Replace the optional signer for `transformERC20()` calldata.
* Only callable by the owner. * Only callable by the owner.