BalancerBridge (#2613)

* Add BalancerBridge and Sampler functions

* Update sampler artifacts/wrappers

* Add Balancer support to AssetSwapper + related refactoring

* Make use of GraphQL instead of sampler

* "fix" build and add mainnet BalancerBridge tests

* address some comments

* add balancer cache and fix DexSampler tests

* lint

* wip: tests for balancer sampler ops

* Fix market operation utils test

* balancer unit tests

* Return a buy quote of 0 if the buy amount exceeds the Balancer pool's balance

* Dynamic fee estimation

* Update contract addresses, export BalancerBridge wrapper

* Update changelogs

* Fix bugs discovered via simbot

* Fix issues in balancer_utils

* override `BigNumber.config` in configured_bignumber.ts

* Special case Balancer subops in  too

* Address some more comments

* Address Balancer performance issue

* Performance improvements

* Address comment

* Fix tests

Co-authored-by: xianny <xianny@gmail.com>
This commit is contained in:
mzhu25 2020-07-14 19:18:50 -07:00 committed by GitHub
parent 18bc701e8b
commit ff9c9241d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1279 additions and 602 deletions

View File

@ -1,10 +1,14 @@
[ [
{ {
"version": "3.3.1", "version": "3.4.0",
"changes": [ "changes": [
{ {
"note": "Fix instability with DFB.", "note": "Fix instability with DFB.",
"pr": 2616 "pr": 2616
},
{
"note": "Add `BalancerBridge`",
"pr": 2613
} }
] ]
}, },

View File

@ -0,0 +1,103 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "../interfaces/IERC20Bridge.sol";
import "../interfaces/IBalancerPool.sol";
contract BalancerBridge is
IERC20Bridge,
IWallet,
DeploymentConstants
{
/// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of
/// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress`
/// token encoded in the bridge data, then transfers the bought
/// tokens to `to`.
/// @param toTokenAddress The token to buy and transfer to `to`.
/// @param from The maker (this contract).
/// @param to The recipient of the bought tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param bridgeData The abi-encoded addresses of the "from" token and Balancer pool.
/// @return success The magic bytes if successful.
function bridgeTransferFrom(
address toTokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success)
{
// Decode the bridge data.
(address fromTokenAddress, address poolAddress) = abi.decode(
bridgeData,
(address, address)
);
require(toTokenAddress != fromTokenAddress, "BalancerBridge/INVALID_PAIR");
uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this));
// Grant an allowance to the exchange to spend `fromTokenAddress` token.
LibERC20Token.approveIfBelow(fromTokenAddress, poolAddress, fromTokenBalance);
// Sell all of this contract's `fromTokenAddress` token balance.
(uint256 boughtAmount,) = IBalancerPool(poolAddress).swapExactAmountIn(
fromTokenAddress, // tokenIn
fromTokenBalance, // tokenAmountIn
toTokenAddress, // tokenOut
amount, // minAmountOut
uint256(-1) // maxPrice
);
// Transfer the converted `toToken`s to `to`.
LibERC20Token.transfer(toTokenAddress, to, boughtAmount);
emit ERC20BridgeTransfer(
fromTokenAddress,
toTokenAddress,
fromTokenBalance,
boughtAmount,
from,
to
);
return BRIDGE_SUCCESS;
}
/// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker
/// and sign for itself in orders. Always succeeds.
/// @return magicValue Magic success bytes, always.
function isValidSignature(
bytes32,
bytes calldata
)
external
view
returns (bytes4 magicValue)
{
return LEGACY_WALLET_MAGIC_VALUE;
}
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IBalancerPool {
/// @dev Sell `tokenAmountIn` of `tokenIn` and receive `tokenOut`.
/// @param tokenIn The token being sold
/// @param tokenAmountIn The amount of `tokenIn` to sell.
/// @param tokenOut The token being bought.
/// @param minAmountOut The minimum amount of `tokenOut` to buy.
/// @param maxPrice The maximum value for `spotPriceAfter`.
/// @return tokenAmountOut The amount of `tokenOut` bought.
/// @return spotPriceAfter The new marginal spot price of the given
/// token pair for this pool.
function swapExactAmountIn(
address tokenIn,
uint tokenAmountIn,
address tokenOut,
uint minAmountOut,
uint maxPrice
) external returns (uint tokenAmountOut, uint spotPriceAfter);
}

View File

@ -38,7 +38,7 @@
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES"
}, },
"config": { "config": {
"abis": "./test/generated-artifacts/@(ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", "abis": "./test/generated-artifacts/@(BalancerBridge|ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
}, },
"repository": { "repository": {

View File

@ -5,6 +5,7 @@
*/ */
import { ContractArtifact } from 'ethereum-types'; import { ContractArtifact } from 'ethereum-types';
import * as BalancerBridge from '../generated-artifacts/BalancerBridge.json';
import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json'; import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json';
import * as CurveBridge from '../generated-artifacts/CurveBridge.json'; import * as CurveBridge from '../generated-artifacts/CurveBridge.json';
import * as DexForwarderBridge from '../generated-artifacts/DexForwarderBridge.json'; import * as DexForwarderBridge from '../generated-artifacts/DexForwarderBridge.json';
@ -18,6 +19,7 @@ import * as IAssetData from '../generated-artifacts/IAssetData.json';
import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json'; import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json';
import * as IBalancerPool from '../generated-artifacts/IBalancerPool.json';
import * as IChai from '../generated-artifacts/IChai.json'; import * as IChai from '../generated-artifacts/IChai.json';
import * as ICurve from '../generated-artifacts/ICurve.json'; import * as ICurve from '../generated-artifacts/ICurve.json';
import * as IDydx from '../generated-artifacts/IDydx.json'; import * as IDydx from '../generated-artifacts/IDydx.json';
@ -57,6 +59,7 @@ export const artifacts = {
ERC721Proxy: ERC721Proxy as ContractArtifact, ERC721Proxy: ERC721Proxy as ContractArtifact,
MultiAssetProxy: MultiAssetProxy as ContractArtifact, MultiAssetProxy: MultiAssetProxy as ContractArtifact,
StaticCallProxy: StaticCallProxy as ContractArtifact, StaticCallProxy: StaticCallProxy as ContractArtifact,
BalancerBridge: BalancerBridge as ContractArtifact,
ChaiBridge: ChaiBridge as ContractArtifact, ChaiBridge: ChaiBridge as ContractArtifact,
CurveBridge: CurveBridge as ContractArtifact, CurveBridge: CurveBridge as ContractArtifact,
DexForwarderBridge: DexForwarderBridge as ContractArtifact, DexForwarderBridge: DexForwarderBridge as ContractArtifact,
@ -70,6 +73,7 @@ export const artifacts = {
IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact,
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact, IAuthorizable: IAuthorizable as ContractArtifact,
IBalancerPool: IBalancerPool as ContractArtifact,
IChai: IChai as ContractArtifact, IChai: IChai as ContractArtifact,
ICurve: ICurve as ContractArtifact, ICurve: ICurve as ContractArtifact,
IDydx: IDydx as ContractArtifact, IDydx: IDydx as ContractArtifact,

View File

@ -1,5 +1,6 @@
export { artifacts } from './artifacts'; export { artifacts } from './artifacts';
export { export {
BalancerBridgeContract,
ChaiBridgeContract, ChaiBridgeContract,
ERC1155ProxyContract, ERC1155ProxyContract,
ERC20BridgeProxyContract, ERC20BridgeProxyContract,

View File

@ -3,6 +3,7 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually. * Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* ----------------------------------------------------------------------------- * -----------------------------------------------------------------------------
*/ */
export * from '../generated-wrappers/balancer_bridge';
export * from '../generated-wrappers/chai_bridge'; export * from '../generated-wrappers/chai_bridge';
export * from '../generated-wrappers/curve_bridge'; export * from '../generated-wrappers/curve_bridge';
export * from '../generated-wrappers/dex_forwarder_bridge'; export * from '../generated-wrappers/dex_forwarder_bridge';
@ -16,6 +17,7 @@ export * from '../generated-wrappers/i_asset_data';
export * from '../generated-wrappers/i_asset_proxy'; export * from '../generated-wrappers/i_asset_proxy';
export * from '../generated-wrappers/i_asset_proxy_dispatcher'; export * from '../generated-wrappers/i_asset_proxy_dispatcher';
export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_balancer_pool';
export * from '../generated-wrappers/i_chai'; export * from '../generated-wrappers/i_chai';
export * from '../generated-wrappers/i_curve'; export * from '../generated-wrappers/i_curve';
export * from '../generated-wrappers/i_dydx'; export * from '../generated-wrappers/i_dydx';

View File

@ -5,6 +5,7 @@
*/ */
import { ContractArtifact } from 'ethereum-types'; import { ContractArtifact } from 'ethereum-types';
import * as BalancerBridge from '../test/generated-artifacts/BalancerBridge.json';
import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json'; import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json';
import * as CurveBridge from '../test/generated-artifacts/CurveBridge.json'; import * as CurveBridge from '../test/generated-artifacts/CurveBridge.json';
import * as DexForwarderBridge from '../test/generated-artifacts/DexForwarderBridge.json'; import * as DexForwarderBridge from '../test/generated-artifacts/DexForwarderBridge.json';
@ -18,6 +19,7 @@ import * as IAssetData from '../test/generated-artifacts/IAssetData.json';
import * as IAssetProxy from '../test/generated-artifacts/IAssetProxy.json'; import * as IAssetProxy from '../test/generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyDispatcher.json'; import * as IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../test/generated-artifacts/IAuthorizable.json'; import * as IAuthorizable from '../test/generated-artifacts/IAuthorizable.json';
import * as IBalancerPool from '../test/generated-artifacts/IBalancerPool.json';
import * as IChai from '../test/generated-artifacts/IChai.json'; import * as IChai from '../test/generated-artifacts/IChai.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json'; import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IDydx from '../test/generated-artifacts/IDydx.json'; import * as IDydx from '../test/generated-artifacts/IDydx.json';
@ -57,6 +59,7 @@ export const artifacts = {
ERC721Proxy: ERC721Proxy as ContractArtifact, ERC721Proxy: ERC721Proxy as ContractArtifact,
MultiAssetProxy: MultiAssetProxy as ContractArtifact, MultiAssetProxy: MultiAssetProxy as ContractArtifact,
StaticCallProxy: StaticCallProxy as ContractArtifact, StaticCallProxy: StaticCallProxy as ContractArtifact,
BalancerBridge: BalancerBridge as ContractArtifact,
ChaiBridge: ChaiBridge as ContractArtifact, ChaiBridge: ChaiBridge as ContractArtifact,
CurveBridge: CurveBridge as ContractArtifact, CurveBridge: CurveBridge as ContractArtifact,
DexForwarderBridge: DexForwarderBridge as ContractArtifact, DexForwarderBridge: DexForwarderBridge as ContractArtifact,
@ -70,6 +73,7 @@ export const artifacts = {
IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact,
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact, IAuthorizable: IAuthorizable as ContractArtifact,
IBalancerPool: IBalancerPool as ContractArtifact,
IChai: IChai as ContractArtifact, IChai: IChai as ContractArtifact,
ICurve: ICurve as ContractArtifact, ICurve: ICurve as ContractArtifact,
IDydx: IDydx as ContractArtifact, IDydx: IDydx as ContractArtifact,

View File

@ -3,6 +3,7 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually. * Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* ----------------------------------------------------------------------------- * -----------------------------------------------------------------------------
*/ */
export * from '../test/generated-wrappers/balancer_bridge';
export * from '../test/generated-wrappers/chai_bridge'; export * from '../test/generated-wrappers/chai_bridge';
export * from '../test/generated-wrappers/curve_bridge'; export * from '../test/generated-wrappers/curve_bridge';
export * from '../test/generated-wrappers/dex_forwarder_bridge'; export * from '../test/generated-wrappers/dex_forwarder_bridge';
@ -16,6 +17,7 @@ export * from '../test/generated-wrappers/i_asset_data';
export * from '../test/generated-wrappers/i_asset_proxy'; export * from '../test/generated-wrappers/i_asset_proxy';
export * from '../test/generated-wrappers/i_asset_proxy_dispatcher'; export * from '../test/generated-wrappers/i_asset_proxy_dispatcher';
export * from '../test/generated-wrappers/i_authorizable'; export * from '../test/generated-wrappers/i_authorizable';
export * from '../test/generated-wrappers/i_balancer_pool';
export * from '../test/generated-wrappers/i_chai'; export * from '../test/generated-wrappers/i_chai';
export * from '../test/generated-wrappers/i_curve'; export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_dydx'; export * from '../test/generated-wrappers/i_dydx';

View File

@ -3,6 +3,7 @@
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [ "files": [
"generated-artifacts/BalancerBridge.json",
"generated-artifacts/ChaiBridge.json", "generated-artifacts/ChaiBridge.json",
"generated-artifacts/CurveBridge.json", "generated-artifacts/CurveBridge.json",
"generated-artifacts/DexForwarderBridge.json", "generated-artifacts/DexForwarderBridge.json",
@ -16,6 +17,7 @@
"generated-artifacts/IAssetProxy.json", "generated-artifacts/IAssetProxy.json",
"generated-artifacts/IAssetProxyDispatcher.json", "generated-artifacts/IAssetProxyDispatcher.json",
"generated-artifacts/IAuthorizable.json", "generated-artifacts/IAuthorizable.json",
"generated-artifacts/IBalancerPool.json",
"generated-artifacts/IChai.json", "generated-artifacts/IChai.json",
"generated-artifacts/ICurve.json", "generated-artifacts/ICurve.json",
"generated-artifacts/IDydx.json", "generated-artifacts/IDydx.json",
@ -45,6 +47,7 @@
"generated-artifacts/TestUniswapV2Bridge.json", "generated-artifacts/TestUniswapV2Bridge.json",
"generated-artifacts/UniswapBridge.json", "generated-artifacts/UniswapBridge.json",
"generated-artifacts/UniswapV2Bridge.json", "generated-artifacts/UniswapV2Bridge.json",
"test/generated-artifacts/BalancerBridge.json",
"test/generated-artifacts/ChaiBridge.json", "test/generated-artifacts/ChaiBridge.json",
"test/generated-artifacts/CurveBridge.json", "test/generated-artifacts/CurveBridge.json",
"test/generated-artifacts/DexForwarderBridge.json", "test/generated-artifacts/DexForwarderBridge.json",
@ -58,6 +61,7 @@
"test/generated-artifacts/IAssetProxy.json", "test/generated-artifacts/IAssetProxy.json",
"test/generated-artifacts/IAssetProxyDispatcher.json", "test/generated-artifacts/IAssetProxyDispatcher.json",
"test/generated-artifacts/IAuthorizable.json", "test/generated-artifacts/IAuthorizable.json",
"test/generated-artifacts/IBalancerPool.json",
"test/generated-artifacts/IChai.json", "test/generated-artifacts/IChai.json",
"test/generated-artifacts/ICurve.json", "test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IDydx.json", "test/generated-artifacts/IDydx.json",

View File

@ -1,4 +1,13 @@
[ [
{
"version": "2.6.0",
"changes": [
{
"note": "Add `BalancerBridge` mainnet tests",
"pr": 2613
}
]
},
{ {
"version": "2.5.2", "version": "2.5.2",
"changes": [ "changes": [

View File

@ -0,0 +1,103 @@
import { artifacts as assetProxyArtifacts } from '@0x/contracts-asset-proxy';
import { BalancerBridgeContract } from '@0x/contracts-asset-proxy/lib/src/wrappers';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { AbiEncoder } from '@0x/utils';
const CHONKY_DAI_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e'; // dydx solo margin
const CHONKY_WETH_WALLET = '0x2f0b23f53734252bda2277357e97e1517d6b042a'; // MCD wETH vault
const CHONKY_USDC_WALLET = '0x39aa39c021dfbae8fac545936693ac917d5e7563'; // Compound
blockchainTests.configure({
fork: {
unlockedAccounts: [CHONKY_USDC_WALLET, CHONKY_WETH_WALLET, CHONKY_DAI_WALLET],
},
});
blockchainTests.fork('Mainnet Balancer bridge tests', env => {
let testContract: BalancerBridgeContract;
let weth: ERC20TokenContract;
let usdc: ERC20TokenContract;
const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308';
const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const wethAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const wethUsdcBalancerAddress = '0x2471de1547296aadb02cc1af84afe369b6f67c87';
const wethUsdcDaiBalancerAddress = '0x9b208194acc0a8ccb2a8dcafeacfbb7dcc093f81';
const bridgeDataEncoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
{ name: 'poolAddress', type: 'address' },
]);
before(async () => {
testContract = await BalancerBridgeContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.BalancerBridge,
env.provider,
{ ...env.txDefaults, from: CHONKY_DAI_WALLET, gasPrice: 0 },
{},
);
weth = new ERC20TokenContract(wethAddress, env.provider, env.txDefaults);
usdc = new ERC20TokenContract(usdcAddress, env.provider, env.txDefaults);
});
blockchainTests.resets('Can trade with two-asset pool', () => {
it('successfully exchanges WETH for USDC', async () => {
const bridgeData = bridgeDataEncoder.encode([wethAddress, wethUsdcBalancerAddress]);
// Fund the Bridge
await weth
.transfer(testContract.address, toBaseUnitAmount(1))
.awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
const usdcBalanceBefore = await usdc.balanceOf(receiver).callAsync();
// Exchange via Balancer
await testContract
.bridgeTransferFrom(usdcAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
// Check that USDC balance increased
const usdcBalanceAfter = await usdc.balanceOf(receiver).callAsync();
expect(usdcBalanceAfter).to.be.bignumber.greaterThan(usdcBalanceBefore);
});
it('successfully exchanges USDC for WETH', async () => {
const bridgeData = bridgeDataEncoder.encode([usdcAddress, wethUsdcBalancerAddress]);
// Fund the Bridge
await usdc
.transfer(testContract.address, toBaseUnitAmount(1, 6))
.awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false });
const wethBalanceBefore = await weth.balanceOf(receiver).callAsync();
// Exchange via Balancer
await testContract
.bridgeTransferFrom(wethAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false });
const wethBalanceAfter = await weth.balanceOf(receiver).callAsync();
expect(wethBalanceAfter).to.be.bignumber.greaterThan(wethBalanceBefore);
});
});
blockchainTests.resets('Can trade with three-asset pool', () => {
it('successfully exchanges WETH for USDC', async () => {
const bridgeData = bridgeDataEncoder.encode([wethAddress, wethUsdcDaiBalancerAddress]);
// Fund the Bridge
await weth
.transfer(testContract.address, toBaseUnitAmount(1))
.awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
const usdcBalanceBefore = await usdc.balanceOf(receiver).callAsync();
// Exchange via Balancer
await testContract
.bridgeTransferFrom(usdcAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
// Check that USDC balance increased
const usdcBalanceAfter = await usdc.balanceOf(receiver).callAsync();
expect(usdcBalanceAfter).to.be.bignumber.greaterThan(usdcBalanceBefore);
});
it('successfully exchanges USDC for WETH', async () => {
const bridgeData = bridgeDataEncoder.encode([usdcAddress, wethUsdcDaiBalancerAddress]);
// Fund the Bridge
await usdc
.transfer(testContract.address, toBaseUnitAmount(1, 6))
.awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false });
const wethBalanceBefore = await weth.balanceOf(receiver).callAsync();
// Exchange via Balancer
await testContract
.bridgeTransferFrom(wethAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false });
const wethBalanceAfter = await weth.balanceOf(receiver).callAsync();
expect(wethBalanceAfter).to.be.bignumber.greaterThan(wethBalanceBefore);
});
});
});

View File

@ -25,6 +25,18 @@
{ {
"note": "\"Fix\" forwarder buys of low decimal tokens.", "note": "\"Fix\" forwarder buys of low decimal tokens.",
"pr": 2618 "pr": 2618
},
{
"note": "Add Balancer support",
"pr": 2613
},
{
"note": "Consolidate UniswapV2 sources, Curve sources in `ERC20BridgeSource` enum",
"pr": 2613
},
{
"note": "Change gas/fee schedule values from constants to functions returning numbers",
"pr": 2613
} }
] ]
}, },

View File

@ -55,8 +55,10 @@
"@0x/quote-server": "^2.0.2", "@0x/quote-server": "^2.0.2",
"@0x/utils": "^5.5.0", "@0x/utils": "^5.5.0",
"@0x/web3-wrapper": "^7.1.0", "@0x/web3-wrapper": "^7.1.0",
"@balancer-labs/sor": "^0.3.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"axios-mock-adapter": "^1.18.1", "axios-mock-adapter": "^1.18.1",
"decimal.js": "^10.2.0",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"heartbeats": "^5.0.1", "heartbeats": "^5.0.1",
"lodash": "^4.17.11" "lodash": "^4.17.11"

View File

@ -69,6 +69,12 @@ export {
NativeCollapsedFill, NativeCollapsedFill,
OptimizedMarketOrder, OptimizedMarketOrder,
GetMarketOrdersRfqtOpts, GetMarketOrdersRfqtOpts,
FeeSchedule,
FillData,
NativeFillData,
CurveFillData,
BalancerFillData,
UniswapV2FillData,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';

View File

@ -0,0 +1,102 @@
import { BigNumber } from '@0x/utils';
import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Decimal } from 'decimal.js';
import * as _ from 'lodash';
export interface BalancerPool {
id: string;
balanceIn: BigNumber;
balanceOut: BigNumber;
weightIn: BigNumber;
weightOut: BigNumber;
swapFee: BigNumber;
spotPrice?: BigNumber;
slippage?: BigNumber;
limitAmount?: BigNumber;
}
interface CacheValue {
timestamp: number;
pools: BalancerPool[];
}
// tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000;
const DEFAULT_TIMEOUT_MS = 1000;
const MAX_POOLS_FETCHED = 3;
const Decimal20 = Decimal.clone({ precision: 20 });
// tslint:enable:custom-no-magic-numbers
export class BalancerPoolsCache {
constructor(
private readonly _cache: { [key: string]: CacheValue } = {},
public cacheExpiryMs: number = FIVE_SECONDS_MS,
) {}
public async getPoolsForPairAsync(
takerToken: string,
makerToken: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<BalancerPool[]> {
const timeout = new Promise<BalancerPool[]>(resolve => setTimeout(resolve, timeoutMs, []));
return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]);
}
protected async _getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - this.cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const timestamp = Date.now();
this._cache[key] = {
pools,
timestamp,
};
}
return this._cache[key].pools;
}
// tslint:disable-next-line:prefer-function-over-method
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
try {
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
// Sort by maker token balance (descending)
const pools = parsePoolData(poolData, takerToken, makerToken).sort((a, b) =>
b.balanceOut.minus(a.balanceOut).toNumber(),
);
return pools.length > MAX_POOLS_FETCHED ? pools.slice(0, MAX_POOLS_FETCHED) : pools;
} catch (err) {
return [];
}
}
}
// tslint:disable completed-docs
export function computeBalancerSellQuote(pool: BalancerPool, takerFillAmount: BigNumber): BigNumber {
const weightRatio = pool.weightIn.dividedBy(pool.weightOut);
const adjustedIn = bmath.BONE.minus(pool.swapFee)
.dividedBy(bmath.BONE)
.times(takerFillAmount);
const y = pool.balanceIn.dividedBy(pool.balanceIn.plus(adjustedIn));
const foo = Math.pow(y.toNumber(), weightRatio.toNumber());
const bar = new BigNumber(1).minus(foo);
const tokenAmountOut = pool.balanceOut.times(bar);
return tokenAmountOut.integerValue();
}
export function computeBalancerBuyQuote(pool: BalancerPool, makerFillAmount: BigNumber): BigNumber {
if (makerFillAmount.isGreaterThanOrEqualTo(pool.balanceOut)) {
return new BigNumber(0);
}
const weightRatio = pool.weightOut.dividedBy(pool.weightIn);
const diff = pool.balanceOut.minus(makerFillAmount);
const y = pool.balanceOut.dividedBy(diff);
let foo: number | Decimal = Math.pow(y.toNumber(), weightRatio.toNumber()) - 1;
if (!Number.isFinite(foo)) {
foo = new Decimal20(y.toString()).pow(weightRatio.toString()).minus(1);
}
let tokenAmountIn = bmath.BONE.minus(pool.swapFee).dividedBy(bmath.BONE);
tokenAmountIn = pool.balanceIn.times(foo.toString()).dividedBy(tokenAmountIn);
return tokenAmountIn.integerValue();
}

View File

@ -10,15 +10,10 @@ import { ERC20BridgeSource, FakeBuyOpts, GetMarketOrdersOpts } from './types';
export const SELL_SOURCES = [ export const SELL_SOURCES = [
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.UniswapV2Eth,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
// All Curve Sources ERC20BridgeSource.Curve,
ERC20BridgeSource.CurveUsdcDai, ERC20BridgeSource.Balancer,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
ERC20BridgeSource.CurveUsdcDaiUsdtBusd,
ERC20BridgeSource.CurveUsdcDaiUsdtSusd,
]; ];
/** /**
@ -27,15 +22,10 @@ export const SELL_SOURCES = [
export const BUY_SOURCES = [ export const BUY_SOURCES = [
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.UniswapV2Eth,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
// All Curve sources ERC20BridgeSource.Curve,
ERC20BridgeSource.CurveUsdcDai, ERC20BridgeSource.Balancer,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtBusd,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
ERC20BridgeSource.CurveUsdcDaiUsdtSusd,
]; ];
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
@ -60,56 +50,44 @@ export const DEFAULT_FAKE_BUY_OPTS: FakeBuyOpts = {
/** /**
* Sources to poll for ETH fee price estimates. * Sources to poll for ETH fee price estimates.
*/ */
export const FEE_QUOTE_SOURCES = SELL_SOURCES; export const FEE_QUOTE_SOURCES = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
];
/** /**
* Mainnet Curve configuration * Mainnet Curve configuration
*/ */
export const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = { export const MAINNET_CURVE_CONTRACTS: { [curveAddress: string]: string[] } = {
[ERC20BridgeSource.CurveUsdcDai]: { '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56': [
version: 1, '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'], ],
}, '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c': [
[ERC20BridgeSource.CurveUsdcDaiUsdt]: { '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
version: 1, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c', '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
tokens: [ ],
'0x6b175474e89094c44da98b954eedeac495271d0f', '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51': [
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xdac17f958d2ee523a2206206994597c13d831ec7', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
], '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
}, '0x0000000000085d4780b73119b644ae5ecd22b376', // TUSD
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: { ],
version: 1, '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27': [
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51', '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
tokens: [ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0x6b175474e89094c44da98b954eedeac495271d0f', '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '0x4fabb145d64652a948d72533023f6e7a623c7c53', // BUSD
'0xdac17f958d2ee523a2206206994597c13d831ec7', ],
'0x0000000000085d4780b73119b644ae5ecd22b376', '0xa5407eae9ba41422680e2e00537571bcc53efbfd': [
], '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
}, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: { '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
version: 1, '0x57ab1ec28d129707052df4df418d58a2d46d5f51', // SUSD
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27', ],
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: {
version: 1,
curveAddress: '0xa5407eae9ba41422680e2e00537571bcc53efbfd',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x57ab1ec28d129707052df4df418d58a2d46d5f51',
],
},
}; };
export const ERC20_PROXY_ID = '0xf47261b0'; export const ERC20_PROXY_ID = '0xf47261b0';

View File

@ -0,0 +1,8 @@
import { MAINNET_CURVE_CONTRACTS } from './constants';
// tslint:disable completed-docs
export function getCurveAddressesForPair(takerToken: string, makerToken: string): string[] {
return Object.keys(MAINNET_CURVE_CONTRACTS).filter(a =>
[makerToken, takerToken].every(t => MAINNET_CURVE_CONTRACTS[a].includes(t)),
);
}

View File

@ -4,15 +4,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types';
CollapsedFill,
DexSample,
ERC20BridgeSource,
Fill,
FillFlags,
NativeCollapsedFill,
NativeFillData,
} from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs // tslint:disable: prefer-for-of no-bitwise completed-docs
@ -26,7 +18,7 @@ export function createFillPaths(opts: {
targetInput?: BigNumber; targetInput?: BigNumber;
ethToOutputRate?: BigNumber; ethToOutputRate?: BigNumber;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
feeSchedule?: { [source: string]: BigNumber }; feeSchedule?: FeeSchedule;
}): Fill[][] { }): Fill[][] {
const { side } = opts; const { side } = opts;
const excludedSources = opts.excludedSources || []; const excludedSources = opts.excludedSources || [];
@ -62,7 +54,7 @@ function nativeOrdersToPath(
orders: SignedOrderWithFillableAmounts[], orders: SignedOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF, targetInput: BigNumber = POSITIVE_INF,
ethToOutputRate: BigNumber, ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber }, fees: FeeSchedule,
): Fill[] { ): Fill[] {
// Create a single path from all orders. // Create a single path from all orders.
let path: Fill[] = []; let path: Fill[] = [];
@ -71,7 +63,9 @@ function nativeOrdersToPath(
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount; const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.Native] || 0); const penalty = ethToOutputRate.times(
fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(),
);
const rate = makerAmount.div(takerAmount); const rate = makerAmount.div(takerAmount);
// targetInput can be less than the order size // targetInput can be less than the order size
// whilst the penalty is constant, it affects the adjusted output // whilst the penalty is constant, it affects the adjusted output
@ -116,7 +110,7 @@ function dexQuotesToPaths(
side: MarketOperation, side: MarketOperation,
dexQuotes: DexSample[][], dexQuotes: DexSample[][],
ethToOutputRate: BigNumber, ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber }, fees: FeeSchedule,
): Fill[][] { ): Fill[][] {
const paths: Fill[][] = []; const paths: Fill[][] = [];
for (let quote of dexQuotes) { for (let quote of dexQuotes) {
@ -129,12 +123,13 @@ function dexQuotesToPaths(
for (let i = 0; i < quote.length; i++) { for (let i = 0; i < quote.length; i++) {
const sample = quote[i]; const sample = quote[i];
const prevSample = i === 0 ? undefined : quote[i - 1]; const prevSample = i === 0 ? undefined : quote[i - 1];
const source = sample.source; const { source, fillData } = sample;
const input = sample.input.minus(prevSample ? prevSample.input : 0); const input = sample.input.minus(prevSample ? prevSample.input : 0);
const output = sample.output.minus(prevSample ? prevSample.output : 0); const output = sample.output.minus(prevSample ? prevSample.output : 0);
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData);
const penalty = const penalty =
i === 0 // Only the first fill in a DEX path incurs a penalty. i === 0 // Only the first fill in a DEX path incurs a penalty.
? ethToOutputRate.times(fees[source] || 0) ? ethToOutputRate.times(fee)
: ZERO_AMOUNT; : ZERO_AMOUNT;
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output); const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output);
@ -147,6 +142,7 @@ function dexQuotesToPaths(
adjustedRate, adjustedRate,
adjustedOutput, adjustedOutput,
source, source,
fillData,
index: i, index: i,
parent: i !== 0 ? path[path.length - 1] : undefined, parent: i !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(source), flags: sourceToFillFlags(source),
@ -241,7 +237,7 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_
} }
export function collapsePath(path: Fill[]): CollapsedFill[] { export function collapsePath(path: Fill[]): CollapsedFill[] {
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = []; const collapsed: CollapsedFill[] = [];
for (const fill of path) { for (const fill of path) {
const source = fill.source; const source = fill.source;
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
@ -256,10 +252,10 @@ export function collapsePath(path: Fill[]): CollapsedFill[] {
} }
collapsed.push({ collapsed.push({
source: fill.source, source: fill.source,
fillData: fill.fillData,
input: fill.input, input: fill.input,
output: fill.output, output: fill.output,
subFills: [fill], subFills: [fill],
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
}); });
} }
return collapsed; return collapsed;

View File

@ -20,6 +20,7 @@ import {
AggregationError, AggregationError,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
OptimizedMarketOrder, OptimizedMarketOrder,
OrderDomain, OrderDomain,
@ -77,6 +78,7 @@ 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);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
@ -89,26 +91,32 @@ export class MarketOperationUtils {
takerToken, takerToken,
), ),
// Get ETH -> maker token price. // Get ETH -> maker token price.
DexOrderSampler.ops.getMedianSellRate( await DexOrderSampler.ops.getMedianSellRateAsync(
difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources),
makerToken, makerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
// Get sell quotes for taker -> maker. // Get sell quotes for taker -> maker.
DexOrderSampler.ops.getSellQuotes( await DexOrderSampler.ops.getSellQuotesAsync(
difference(SELL_SOURCES.concat(this._optionalSources()), _opts.excludedSources), difference(
SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(ERC20BridgeSource.Balancer),
),
makerToken, makerToken,
takerToken, takerToken,
getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), sampleAmounts,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
); );
const rfqtPromise = getRfqtIndicativeQuotesAsync( const rfqtPromise = getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData, nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData, nativeOrders[0].takerAssetData,
@ -116,14 +124,29 @@ export class MarketOperationUtils {
takerAmount, takerAmount,
_opts, _opts,
); );
const balancerPromise = this._sampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
difference([ERC20BridgeSource.Balancer], _opts.excludedSources),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
),
);
const [ const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes], [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
] = await Promise.all([samplerPromise, rfqtPromise]); [balancerQuotes],
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]);
return this._generateOptimizedOrders({ return this._generateOptimizedOrders({
orderFillableAmounts, orderFillableAmounts,
nativeOrders, nativeOrders,
dexQuotes, dexQuotes: dexQuotes.concat(balancerQuotes),
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
liquidityProviderAddress, liquidityProviderAddress,
multiBridgeAddress: this._multiBridge, multiBridgeAddress: this._multiBridge,
@ -159,6 +182,8 @@ 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);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts. // Get native order fillable amounts.
@ -170,30 +195,45 @@ export class MarketOperationUtils {
takerToken, takerToken,
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
DexOrderSampler.ops.getMedianSellRate( await DexOrderSampler.ops.getMedianSellRateAsync(
difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources),
takerToken, takerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
// Get buy quotes for taker -> maker. // Get buy quotes for taker -> maker.
DexOrderSampler.ops.getBuyQuotes( await DexOrderSampler.ops.getBuyQuotesAsync(
difference( difference(
BUY_SOURCES.concat( BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [], this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [],
), ),
_opts.excludedSources, _opts.excludedSources.concat(ERC20BridgeSource.Balancer),
), ),
makerToken, makerToken,
takerToken, takerToken,
getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase), sampleAmounts,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
), ),
); );
const balancerPromise = this._sampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
difference([ERC20BridgeSource.Balancer], _opts.excludedSources),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
),
);
const rfqtPromise = getRfqtIndicativeQuotesAsync( const rfqtPromise = getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData, nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData, nativeOrders[0].takerAssetData,
@ -204,12 +244,13 @@ export class MarketOperationUtils {
const [ const [
[orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes], [orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
] = await Promise.all([samplerPromise, rfqtPromise]); [balancerQuotes],
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]);
return this._generateOptimizedOrders({ return this._generateOptimizedOrders({
orderFillableAmounts, orderFillableAmounts,
nativeOrders, nativeOrders,
dexQuotes, dexQuotes: dexQuotes.concat(balancerQuotes),
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
liquidityProviderAddress, liquidityProviderAddress,
multiBridgeAddress: this._multiBridge, multiBridgeAddress: this._multiBridge,
@ -251,24 +292,30 @@ export class MarketOperationUtils {
const sources = difference(BUY_SOURCES, _opts.excludedSources); const sources = difference(BUY_SOURCES, _opts.excludedSources);
const ops = [ const ops = [
...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)), ...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)),
...batchNativeOrders.map(orders => ...(await Promise.all(
DexOrderSampler.ops.getMedianSellRate( batchNativeOrders.map(async orders =>
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), DexOrderSampler.ops.getMedianSellRateAsync(
getNativeOrderTokens(orders[0])[1], difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
this._wethAddress, getNativeOrderTokens(orders[0])[1],
ONE_ETHER, this._wethAddress,
this._wethAddress, ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
),
), ),
), )),
...batchNativeOrders.map((orders, i) => ...(await Promise.all(
DexOrderSampler.ops.getBuyQuotes( batchNativeOrders.map(async (orders, i) =>
sources, DexOrderSampler.ops.getBuyQuotesAsync(
getNativeOrderTokens(orders[0])[0], sources,
getNativeOrderTokens(orders[0])[1], getNativeOrderTokens(orders[0])[0],
[makerAmounts[i]], getNativeOrderTokens(orders[0])[1],
this._wethAddress, [makerAmounts[i]],
this._wethAddress,
this._sampler.balancerPoolsCache,
),
), ),
), )),
]; ];
const executeResults = await this._sampler.executeBatchAsync(ops); const executeResults = await this._sampler.executeBatchAsync(ops);
@ -325,7 +372,7 @@ export class MarketOperationUtils {
bridgeSlippage?: number; bridgeSlippage?: number;
maxFallbackSlippage?: number; maxFallbackSlippage?: number;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
feeSchedule?: { [source: string]: BigNumber }; feeSchedule?: FeeSchedule;
allowFallback?: boolean; allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean; shouldBatchBridgeOrders?: boolean;
liquidityProviderAddress?: string; liquidityProviderAddress?: string;

View File

@ -1,20 +1,20 @@
import { NULL_ADDRESS } from './constants'; import { NULL_ADDRESS } from './constants';
// tslint:disable completed-docs // tslint:disable completed-docs
// tslint:disable enum-naming
export const TOKENS = { enum Tokens {
WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', DAI = '0x6b175474e89094c44da98b954eedeac495271d0f',
USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
MKR: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', MKR = '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
}; }
export function getMultiBridgeIntermediateToken(takerToken: string, makerToken: string): string { export function getMultiBridgeIntermediateToken(takerToken: string, makerToken: string): string {
let intermediateToken = NULL_ADDRESS; let intermediateToken = NULL_ADDRESS;
if (takerToken !== TOKENS.WETH && makerToken !== TOKENS.WETH) { if (takerToken !== Tokens.WETH && makerToken !== Tokens.WETH) {
intermediateToken = TOKENS.WETH; intermediateToken = Tokens.WETH;
} else if (takerToken === TOKENS.USDC || makerToken === TOKENS.USDC) { } else if (takerToken === Tokens.USDC || makerToken === Tokens.USDC) {
intermediateToken = TOKENS.DAI; intermediateToken = Tokens.DAI;
} }
return intermediateToken; return intermediateToken;
} }

View File

@ -5,7 +5,6 @@ import { ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { getCurveInfo } from '../source_utils';
import { import {
ERC20_PROXY_ID, ERC20_PROXY_ID,
@ -20,12 +19,15 @@ import { collapsePath } from './fills';
import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import { import {
AggregationError, AggregationError,
BalancerFillData,
CollapsedFill, CollapsedFill,
CurveFillData,
ERC20BridgeSource, ERC20BridgeSource,
Fill, Fill,
NativeCollapsedFill, NativeCollapsedFill,
OptimizedMarketOrder, OptimizedMarketOrder,
OrderDomain, OrderDomain,
UniswapV2FillData,
} from './types'; } from './types';
// tslint:disable completed-docs no-unnecessary-type-assertion // tslint:disable completed-docs no-unnecessary-type-assertion
@ -151,7 +153,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
const orders: OptimizedMarketOrder[] = []; const orders: OptimizedMarketOrder[] = [];
for (let i = 0; i < collapsedPath.length; ) { for (let i = 0; i < collapsedPath.length; ) {
if (collapsedPath[i].source === ERC20BridgeSource.Native) { if (collapsedPath[i].source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(collapsedPath[i])); orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill));
++i; ++i;
continue; continue;
} }
@ -184,14 +186,11 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder
case ERC20BridgeSource.Uniswap: case ERC20BridgeSource.Uniswap:
return opts.contractAddresses.uniswapBridge; return opts.contractAddresses.uniswapBridge;
case ERC20BridgeSource.UniswapV2: case ERC20BridgeSource.UniswapV2:
case ERC20BridgeSource.UniswapV2Eth:
return opts.contractAddresses.uniswapV2Bridge; return opts.contractAddresses.uniswapV2Bridge;
case ERC20BridgeSource.CurveUsdcDai: case ERC20BridgeSource.Curve:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtSusd:
return opts.contractAddresses.curveBridge; return opts.contractAddresses.curveBridge;
case ERC20BridgeSource.Balancer:
return opts.contractAddresses.balancerBridge;
case ERC20BridgeSource.LiquidityProvider: case ERC20BridgeSource.LiquidityProvider:
if (opts.liquidityProviderAddress === undefined) { if (opts.liquidityProviderAddress === undefined) {
throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.'); throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.');
@ -214,39 +213,33 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
let makerAssetData; let makerAssetData;
switch (fill.source) { switch (fill.source) {
case ERC20BridgeSource.CurveUsdcDai: case ERC20BridgeSource.Curve:
case ERC20BridgeSource.CurveUsdcDaiUsdt: const curveFillData = (fill as CollapsedFill<CurveFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtSusd:
const { curveAddress, fromTokenIdx, toTokenIdx, version } = getCurveInfo(
fill.source,
takerToken,
makerToken,
);
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken, makerToken,
bridgeAddress, bridgeAddress,
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version), createCurveBridgeData(
curveFillData.poolAddress,
curveFillData.fromTokenIdx,
curveFillData.toTokenIdx,
1, // "version"
),
);
break;
case ERC20BridgeSource.Balancer:
const balancerFillData = (fill as CollapsedFill<BalancerFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBalancerBridgeData(takerToken, balancerFillData.poolAddress),
); );
break; break;
case ERC20BridgeSource.UniswapV2: case ERC20BridgeSource.UniswapV2:
const uniswapV2FillData = (fill as CollapsedFill<UniswapV2FillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken, makerToken,
bridgeAddress, bridgeAddress,
createUniswapV2BridgeData([takerToken, makerToken]), createUniswapV2BridgeData(uniswapV2FillData.tokenAddressPath),
);
break;
case ERC20BridgeSource.UniswapV2Eth:
if (opts.contractAddresses.etherToken === NULL_ADDRESS) {
throw new Error(
`Cannot create a ${ERC20BridgeSource.UniswapV2Eth.toString()} order without a WETH address`,
);
}
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createUniswapV2BridgeData([takerToken, opts.contractAddresses.etherToken, makerToken]),
); );
break; break;
case ERC20BridgeSource.MultiBridge: case ERC20BridgeSource.MultiBridge:
@ -338,6 +331,14 @@ function createMultiBridgeData(takerToken: string, makerToken: string): string {
return encoder.encode({ takerToken, intermediateToken }); return encoder.encode({ takerToken, intermediateToken });
} }
function createBalancerBridgeData(takerToken: string, poolAddress: string): string {
const encoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
{ name: 'poolAddress', type: 'address' },
]);
return encoder.encode({ takerToken, poolAddress });
}
function createCurveBridgeData( function createCurveBridgeData(
curveAddress: string, curveAddress: string,
fromTokenIdx: number, fromTokenIdx: number,
@ -404,10 +405,10 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
}; };
} }
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder { function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
return { return {
fills: [fill], fills: [fill],
...(fill as NativeCollapsedFill).nativeOrder, ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion
}; };
} }

View File

@ -3,11 +3,13 @@ import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { ZERO_AMOUNT } from './constants'; import { ZERO_AMOUNT } from './constants';
import { getPathSize, isValidPath } from './fills'; import { getPathAdjustedSize, getPathSize, isValidPath } from './fills';
import { Fill } from './types'; import { Fill } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs
const RUN_LIMIT_DECAY_FACTOR = 0.8;
/** /**
* Find the optimal mixture of paths that maximizes (for sells) or minimizes * Find the optimal mixture of paths that maximizes (for sells) or minimizes
* (for buys) output, while meeting the input requirement. * (for buys) output, while meeting the input requirement.
@ -16,11 +18,15 @@ export function findOptimalPath(
side: MarketOperation, side: MarketOperation,
paths: Fill[][], paths: Fill[][],
targetInput: BigNumber, targetInput: BigNumber,
runLimit?: number, runLimit: number = 2 ** 15,
): Fill[] | undefined { ): Fill[] | undefined {
let optimalPath = paths[0] || []; // Sort paths in descending order by adjusted output amount.
for (const path of paths.slice(1)) { const sortedPaths = paths
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit); .slice(0)
.sort((a, b) => getPathAdjustedSize(b, targetInput)[1].comparedTo(getPathAdjustedSize(a, targetInput)[1]));
let optimalPath = sortedPaths[0] || [];
for (const [i, path] of sortedPaths.slice(1).entries()) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i);
} }
return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined;
} }
@ -30,7 +36,7 @@ function mixPaths(
pathA: Fill[], pathA: Fill[],
pathB: Fill[], pathB: Fill[],
targetInput: BigNumber, targetInput: BigNumber,
maxSteps: number = 2 ** 15, maxSteps: number,
): Fill[] { ): Fill[] {
let bestPath: Fill[] = []; let bestPath: Fill[] = [];
let bestPathInput = ZERO_AMOUNT; let bestPathInput = ZERO_AMOUNT;

View File

@ -1,6 +1,7 @@
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { BalancerPoolsCache } from './balancer_utils';
import { samplerOperations } from './sampler_operations'; import { samplerOperations } from './sampler_operations';
import { BatchedOperation } from './types'; import { BatchedOperation } from './types';
@ -11,6 +12,9 @@ export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, e
const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i)); const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i));
const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution)));
const amounts = stepSizes.map((_s, i) => { const amounts = stepSizes.map((_s, i) => {
if (i === numSamples - 1) {
return maxFillAmount;
}
return maxFillAmount return maxFillAmount
.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])) .times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)]))
.integerValue(BigNumber.ROUND_UP); .integerValue(BigNumber.ROUND_UP);
@ -29,11 +33,11 @@ export class DexOrderSampler {
* for use with `DexOrderSampler.executeAsync()`. * for use with `DexOrderSampler.executeAsync()`.
*/ */
public static ops = samplerOperations; public static ops = samplerOperations;
private readonly _samplerContract: IERC20BridgeSamplerContract;
constructor(samplerContract: IERC20BridgeSamplerContract) { constructor(
this._samplerContract = samplerContract; private readonly _samplerContract: IERC20BridgeSamplerContract,
} public balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(),
) {}
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */

View File

@ -1,9 +1,20 @@
import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; import * as _ from 'lodash';
import { getCurveInfo, isCurveSource } from '../source_utils';
import { DEFAULT_FAKE_BUY_OPTS } from './constants'; import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..';
import { BalancerPool, BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils';
import { DEFAULT_FAKE_BUY_OPTS, MAINNET_CURVE_CONTRACTS } from './constants';
import { getCurveAddressesForPair } from './curve_utils';
import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import { BatchedOperation, DexSample, FakeBuyOpts } from './types'; import {
BalancerFillData,
BatchedOperation,
CurveFillData,
DexSample,
FakeBuyOpts,
SourceQuoteOperation,
UniswapV2FillData,
} from './types';
/** /**
* Composable operations that can be batched in a single transaction, * Composable operations that can be batched in a single transaction,
@ -34,12 +45,9 @@ export const samplerOperations = {
}, },
}; };
}, },
getKyberSellQuotes( getKyberSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation {
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return { return {
source: ERC20BridgeSource.Kyber,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts) .sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts)
@ -55,8 +63,9 @@ export const samplerOperations = {
takerToken: string, takerToken: string,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.Kyber,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts, fakeBuyOpts) .sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts, fakeBuyOpts)
@ -67,12 +76,9 @@ export const samplerOperations = {
}, },
}; };
}, },
getUniswapSellQuotes( getUniswapSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation {
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return { return {
source: ERC20BridgeSource.Uniswap,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts) .sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts)
@ -83,12 +89,9 @@ export const samplerOperations = {
}, },
}; };
}, },
getUniswapBuyQuotes( getUniswapBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation {
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return { return {
source: ERC20BridgeSource.Uniswap,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts) .sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts)
@ -99,8 +102,13 @@ export const samplerOperations = {
}, },
}; };
}, },
getUniswapV2SellQuotes(tokenAddressPath: string[], takerFillAmounts: BigNumber[]): BatchedOperation<BigNumber[]> { getUniswapV2SellQuotes(
tokenAddressPath: string[],
takerFillAmounts: BigNumber[],
): SourceQuoteOperation<UniswapV2FillData> {
return { return {
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromUniswapV2(tokenAddressPath, takerFillAmounts) .sampleSellsFromUniswapV2(tokenAddressPath, takerFillAmounts)
@ -111,8 +119,13 @@ export const samplerOperations = {
}, },
}; };
}, },
getUniswapV2BuyQuotes(tokenAddressPath: string[], makerFillAmounts: BigNumber[]): BatchedOperation<BigNumber[]> { getUniswapV2BuyQuotes(
tokenAddressPath: string[],
makerFillAmounts: BigNumber[],
): SourceQuoteOperation<UniswapV2FillData> {
return { return {
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromUniswapV2(tokenAddressPath, makerFillAmounts) .sampleBuysFromUniswapV2(tokenAddressPath, makerFillAmounts)
@ -128,8 +141,9 @@ export const samplerOperations = {
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
takerFillAmounts: BigNumber[], takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.LiquidityProvider,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, takerFillAmounts) .sampleSellsFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, takerFillAmounts)
@ -143,41 +157,15 @@ export const samplerOperations = {
}, },
}; };
}, },
getMultiBridgeSellQuotes(
multiBridgeAddress: string,
makerToken: string,
intermediateToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleSellsFromMultiBridge(
multiBridgeAddress,
takerToken,
intermediateToken,
makerToken,
takerFillAmounts,
)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>(
'sampleSellsFromLiquidityProviderRegistry',
callResults,
);
},
};
},
getLiquidityProviderBuyQuotes( getLiquidityProviderBuyQuotes(
registryAddress: string, registryAddress: string,
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.LiquidityProvider,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromLiquidityProviderRegistry( .sampleBuysFromLiquidityProviderRegistry(
@ -197,12 +185,34 @@ export const samplerOperations = {
}, },
}; };
}, },
getEth2DaiSellQuotes( getMultiBridgeSellQuotes(
multiBridgeAddress: string,
makerToken: string, makerToken: string,
intermediateToken: string,
takerToken: string, takerToken: string,
takerFillAmounts: BigNumber[], takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.MultiBridge,
encodeCall: contract => {
return contract
.sampleSellsFromMultiBridge(
multiBridgeAddress,
takerToken,
intermediateToken,
makerToken,
takerFillAmounts,
)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromMultiBridge', callResults);
},
};
},
getEth2DaiSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Eth2Dai,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts) .sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts)
@ -213,12 +223,9 @@ export const samplerOperations = {
}, },
}; };
}, },
getEth2DaiBuyQuotes( getEth2DaiBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation {
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return { return {
source: ERC20BridgeSource.Eth2Dai,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts) .sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts)
@ -234,8 +241,14 @@ export const samplerOperations = {
fromTokenIdx: number, fromTokenIdx: number,
toTokenIdx: number, toTokenIdx: number,
takerFillAmounts: BigNumber[], takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation<CurveFillData> {
return { return {
source: ERC20BridgeSource.Curve,
fillData: {
poolAddress: curveAddress,
fromTokenIdx,
toTokenIdx,
},
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromCurve( .sampleSellsFromCurve(
@ -256,8 +269,14 @@ export const samplerOperations = {
fromTokenIdx: number, fromTokenIdx: number,
toTokenIdx: number, toTokenIdx: number,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> { ): SourceQuoteOperation<CurveFillData> {
return { return {
source: ERC20BridgeSource.Curve,
fillData: {
poolAddress: curveAddress,
fromTokenIdx,
toTokenIdx,
},
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromCurve( .sampleBuysFromCurve(
@ -273,24 +292,40 @@ export const samplerOperations = {
}, },
}; };
}, },
getMedianSellRate( getBalancerSellQuotes(pool: BalancerPool, takerFillAmounts: BigNumber[]): SourceQuoteOperation<BalancerFillData> {
return {
source: ERC20BridgeSource.Balancer,
fillData: { poolAddress: pool.id },
...samplerOperations.constant(takerFillAmounts.map(amount => computeBalancerSellQuote(pool, amount))),
};
},
getBalancerBuyQuotes(pool: BalancerPool, makerFillAmounts: BigNumber[]): SourceQuoteOperation<BalancerFillData> {
return {
source: ERC20BridgeSource.Balancer,
fillData: { poolAddress: pool.id },
...samplerOperations.constant(makerFillAmounts.map(amount => computeBalancerBuyQuote(pool, amount))),
};
},
getMedianSellRateAsync: async (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
takerFillAmount: BigNumber, takerFillAmount: BigNumber,
wethAddress: string, wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string, multiBridgeAddress?: string,
): BatchedOperation<BigNumber> { ): Promise<BatchedOperation<BigNumber>> => {
if (makerToken.toLowerCase() === takerToken.toLowerCase()) { if (makerToken.toLowerCase() === takerToken.toLowerCase()) {
return samplerOperations.constant(new BigNumber(1)); return samplerOperations.constant(new BigNumber(1));
} }
const getSellQuotes = samplerOperations.getSellQuotes( const getSellQuotes = await samplerOperations.getSellQuotesAsync(
sources, sources,
makerToken, makerToken,
takerToken, takerToken,
[takerFillAmount], [takerFillAmount],
wethAddress, wethAddress,
balancerPoolsCache,
liquidityProviderRegistryAddress, liquidityProviderRegistryAddress,
multiBridgeAddress, multiBridgeAddress,
); );
@ -343,181 +378,221 @@ export const samplerOperations = {
}, },
}; };
}, },
getSellQuotes( getSellQuotesAsync: async (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
takerFillAmounts: BigNumber[], takerFillAmounts: BigNumber[],
wethAddress: string, wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string, multiBridgeAddress?: string,
): BatchedOperation<DexSample[][]> { ): Promise<BatchedOperation<DexSample[][]>> => {
const subOps = sources const subOps = _.flatten(
.map(source => { await Promise.all(
let batchedOperation; sources.map(
if (source === ERC20BridgeSource.Eth2Dai) { async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => {
batchedOperation = samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts); switch (source) {
} else if (source === ERC20BridgeSource.Uniswap) { case ERC20BridgeSource.Eth2Dai:
batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); return samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.UniswapV2) { case ERC20BridgeSource.Uniswap:
batchedOperation = samplerOperations.getUniswapV2SellQuotes( return samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
[takerToken, makerToken], case ERC20BridgeSource.UniswapV2:
takerFillAmounts, const ops = [
); samplerOperations.getUniswapV2SellQuotes(
} else if (source === ERC20BridgeSource.UniswapV2Eth) { [takerToken, makerToken],
batchedOperation = samplerOperations.getUniswapV2SellQuotes( takerFillAmounts,
[takerToken, wethAddress, makerToken], ),
takerFillAmounts, ];
); if (takerToken !== wethAddress && makerToken !== wethAddress) {
} else if (source === ERC20BridgeSource.Kyber) { ops.push(
batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); samplerOperations.getUniswapV2SellQuotes(
} else if (isCurveSource(source)) { [takerToken, wethAddress, makerToken],
const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken); takerFillAmounts,
if (fromTokenIdx !== -1 && toTokenIdx !== -1) { ),
batchedOperation = samplerOperations.getCurveSellQuotes( );
curveAddress, }
fromTokenIdx, return ops;
toTokenIdx, case ERC20BridgeSource.Kyber:
takerFillAmounts, return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
); case ERC20BridgeSource.Curve:
} return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress =>
} else if (source === ERC20BridgeSource.LiquidityProvider) { samplerOperations.getCurveSellQuotes(
if (liquidityProviderRegistryAddress === undefined) { curveAddress,
throw new Error( MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken),
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken),
); takerFillAmounts,
} ),
batchedOperation = samplerOperations.getLiquidityProviderSellQuotes( );
liquidityProviderRegistryAddress, case ERC20BridgeSource.LiquidityProvider:
makerToken, if (liquidityProviderRegistryAddress === undefined) {
takerToken, throw new Error(
takerFillAmounts, 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
); );
} else if (source === ERC20BridgeSource.MultiBridge) { }
if (multiBridgeAddress === undefined) { return samplerOperations.getLiquidityProviderSellQuotes(
throw new Error('Cannot sample liquidity from MultiBridge if an address is not provided.'); liquidityProviderRegistryAddress,
} makerToken,
const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); takerToken,
batchedOperation = samplerOperations.getMultiBridgeSellQuotes( takerFillAmounts,
multiBridgeAddress, );
makerToken, case ERC20BridgeSource.MultiBridge:
intermediateToken, if (multiBridgeAddress === undefined) {
takerToken, throw new Error(
takerFillAmounts, 'Cannot sample liquidity from MultiBridge if an address is not provided.',
); );
} else { }
throw new Error(`Unsupported sell sample source: ${source}`); const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken);
} return samplerOperations.getMultiBridgeSellQuotes(
return { batchedOperation, source }; multiBridgeAddress,
}) makerToken,
.filter(op => op.batchedOperation) as Array<{ intermediateToken,
batchedOperation: BatchedOperation<BigNumber[]>; takerToken,
source: ERC20BridgeSource; takerFillAmounts,
}>; );
// todo: refactor sampler ops to share state with DexOrderSampler so cache doesn't have to be passed as a param
case ERC20BridgeSource.Balancer:
if (balancerPoolsCache === undefined) {
throw new Error(
'Cannot sample liquidity from Balancer if a cache is not provided.',
);
}
const pools = await balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken);
return pools.map(pool =>
samplerOperations.getBalancerSellQuotes(pool, takerFillAmounts),
);
default:
throw new Error(`Unsupported sell sample source: ${source}`);
}
},
),
),
);
const samplerOps = subOps.filter(op => op.source !== ERC20BridgeSource.Balancer);
const nonSamplerOps = subOps.filter(op => op.source === ERC20BridgeSource.Balancer);
return { return {
encodeCall: contract => { encodeCall: contract => {
const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract)); const subCalls = samplerOps.map(op => op.encodeCall(contract));
return contract.batchCall(subCalls).getABIEncodedTransactionData(); return contract.batchCall(subCalls).getABIEncodedTransactionData();
}, },
handleCallResultsAsync: async (contract, callResults) => { handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults); const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await Promise.all( let samples = await Promise.all(
subOps.map(async (op, i) => samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])),
op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]),
),
); );
return subOps.map((op, i) => { samples = samples.concat(
await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))),
);
return [...samplerOps, ...nonSamplerOps].map((op, i) => {
return samples[i].map((output, j) => ({ return samples[i].map((output, j) => ({
source: op.source, source: op.source,
output, output,
input: takerFillAmounts[j], input: takerFillAmounts[j],
fillData: op.fillData,
})); }));
}); });
}, },
}; };
}, },
getBuyQuotes( getBuyQuotesAsync: async (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
wethAddress: string, wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<DexSample[][]> { ): Promise<BatchedOperation<DexSample[][]>> => {
const subOps = sources const subOps = _.flatten(
.map(source => { await Promise.all(
let batchedOperation; sources.map(
if (source === ERC20BridgeSource.Eth2Dai) { async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => {
batchedOperation = samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts); switch (source) {
} else if (source === ERC20BridgeSource.Uniswap) { case ERC20BridgeSource.Eth2Dai:
batchedOperation = samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); return samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts);
} else if (source === ERC20BridgeSource.UniswapV2) { case ERC20BridgeSource.Uniswap:
batchedOperation = samplerOperations.getUniswapV2BuyQuotes( return samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts);
[takerToken, makerToken], case ERC20BridgeSource.UniswapV2:
makerFillAmounts, const ops = [
); samplerOperations.getUniswapV2BuyQuotes([takerToken, makerToken], makerFillAmounts),
} else if (source === ERC20BridgeSource.UniswapV2Eth) { ];
batchedOperation = samplerOperations.getUniswapV2BuyQuotes( if (takerToken !== wethAddress && makerToken !== wethAddress) {
[takerToken, wethAddress, makerToken], ops.push(
makerFillAmounts, samplerOperations.getUniswapV2BuyQuotes(
); [takerToken, wethAddress, makerToken],
} else if (source === ERC20BridgeSource.Kyber) { makerFillAmounts,
batchedOperation = samplerOperations.getKyberBuyQuotes( ),
makerToken, );
takerToken, }
makerFillAmounts, return ops;
fakeBuyOpts, case ERC20BridgeSource.Kyber:
); return samplerOperations.getKyberBuyQuotes(
} else if (isCurveSource(source)) { makerToken,
const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken); takerToken,
if (fromTokenIdx !== -1 && toTokenIdx !== -1) { makerFillAmounts,
batchedOperation = samplerOperations.getCurveBuyQuotes( fakeBuyOpts,
curveAddress, );
fromTokenIdx, case ERC20BridgeSource.Curve:
toTokenIdx, return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress =>
makerFillAmounts, samplerOperations.getCurveBuyQuotes(
); curveAddress,
} MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken),
} else if (source === ERC20BridgeSource.LiquidityProvider) { MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken),
if (liquidityProviderRegistryAddress === undefined) { makerFillAmounts,
throw new Error( ),
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', );
); case ERC20BridgeSource.LiquidityProvider:
} if (liquidityProviderRegistryAddress === undefined) {
batchedOperation = samplerOperations.getLiquidityProviderBuyQuotes( throw new Error(
liquidityProviderRegistryAddress, 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
makerToken, );
takerToken, }
makerFillAmounts, return samplerOperations.getLiquidityProviderBuyQuotes(
fakeBuyOpts, liquidityProviderRegistryAddress,
); makerToken,
} else { takerToken,
throw new Error(`Unsupported buy sample source: ${source}`); makerFillAmounts,
} fakeBuyOpts,
return { source, batchedOperation }; );
}) case ERC20BridgeSource.Balancer:
.filter(op => op.batchedOperation) as Array<{ if (balancerPoolsCache === undefined) {
batchedOperation: BatchedOperation<BigNumber[]>; throw new Error(
source: ERC20BridgeSource; 'Cannot sample liquidity from Balancer if a cache is not provided.',
}>; );
}
const pools = await balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken);
return pools.map(pool =>
samplerOperations.getBalancerBuyQuotes(pool, makerFillAmounts),
);
default:
throw new Error(`Unsupported buy sample source: ${source}`);
}
},
),
),
);
const samplerOps = subOps.filter(op => op.source !== ERC20BridgeSource.Balancer);
const nonSamplerOps = subOps.filter(op => op.source === ERC20BridgeSource.Balancer);
return { return {
encodeCall: contract => { encodeCall: contract => {
const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract)); const subCalls = samplerOps.map(op => op.encodeCall(contract));
return contract.batchCall(subCalls).getABIEncodedTransactionData(); return contract.batchCall(subCalls).getABIEncodedTransactionData();
}, },
handleCallResultsAsync: async (contract, callResults) => { handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults); const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await Promise.all( let samples = await Promise.all(
subOps.map(async (op, i) => samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])),
op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]),
),
); );
return subOps.map((op, i) => { samples = samples.concat(
await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))),
);
return [...samplerOps, ...nonSamplerOps].map((op, i) => {
return samples[i].map((output, j) => ({ return samples[i].map((output, j) => ({
source: op.source, source: op.source,
output, output,
input: makerFillAmounts[j], input: makerFillAmounts[j],
fillData: op.fillData,
})); }));
}); });
}, },

View File

@ -29,16 +29,12 @@ export enum ERC20BridgeSource {
Native = 'Native', Native = 'Native',
Uniswap = 'Uniswap', Uniswap = 'Uniswap',
UniswapV2 = 'Uniswap_V2', UniswapV2 = 'Uniswap_V2',
UniswapV2Eth = 'Uniswap_V2_ETH',
Eth2Dai = 'Eth2Dai', Eth2Dai = 'Eth2Dai',
Kyber = 'Kyber', Kyber = 'Kyber',
CurveUsdcDai = 'Curve_USDC_DAI', Curve = 'Curve',
CurveUsdcDaiUsdt = 'Curve_USDC_DAI_USDT',
CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD',
CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD',
CurveUsdcDaiUsdtSusd = 'Curve_USDC_DAI_USDT_SUSD',
LiquidityProvider = 'LiquidityProvider', LiquidityProvider = 'LiquidityProvider',
MultiBridge = 'MultiBridge', MultiBridge = 'MultiBridge',
Balancer = 'Balancer',
} }
// Internal `fillData` field for `Fill` objects. // Internal `fillData` field for `Fill` objects.
@ -49,13 +45,28 @@ export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts; order: SignedOrderWithFillableAmounts;
} }
export interface CurveFillData extends FillData {
poolAddress: string;
fromTokenIdx: number;
toTokenIdx: number;
}
export interface BalancerFillData extends FillData {
poolAddress: string;
}
export interface UniswapV2FillData extends FillData {
tokenAddressPath: string[];
}
/** /**
* Represents an individual DEX sample from the sampler contract. * Represents an individual DEX sample from the sampler contract.
*/ */
export interface DexSample { export interface DexSample<TFillData extends FillData = FillData> {
source: ERC20BridgeSource; source: ERC20BridgeSource;
input: BigNumber; input: BigNumber;
output: BigNumber; output: BigNumber;
fillData?: TFillData;
} }
/** /**
@ -71,7 +82,7 @@ export enum FillFlags {
/** /**
* Represents a node on a fill path. * Represents a node on a fill path.
*/ */
export interface Fill { export interface Fill<TFillData extends FillData = FillData> {
// See `FillFlags`. // See `FillFlags`.
flags: FillFlags; flags: FillFlags;
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy). // Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
@ -92,13 +103,13 @@ export interface Fill {
source: ERC20BridgeSource; source: ERC20BridgeSource;
// Data associated with this this Fill object. Used to reconstruct orders // Data associated with this this Fill object. Used to reconstruct orders
// from paths. // from paths.
fillData?: FillData | NativeFillData; fillData?: TFillData;
} }
/** /**
* Represents continguous fills on a path that have been merged together. * Represents continguous fills on a path that have been merged together.
*/ */
export interface CollapsedFill { export interface CollapsedFill<TFillData extends FillData = FillData> {
/** /**
* The source DEX. * The source DEX.
*/ */
@ -118,14 +129,14 @@ export interface CollapsedFill {
input: BigNumber; input: BigNumber;
output: BigNumber; output: BigNumber;
}>; }>;
fillData?: TFillData;
} }
/** /**
* A `CollapsedFill` wrapping a native order. * A `CollapsedFill` wrapping a native order.
*/ */
export interface NativeCollapsedFill extends CollapsedFill { export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {}
nativeOrder: SignedOrderWithFillableAmounts;
}
/** /**
* Optimized orders to fill. * Optimized orders to fill.
@ -141,6 +152,9 @@ export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts {
quoteRequestor?: QuoteRequestor; quoteRequestor?: QuoteRequestor;
} }
export type FeeEstimate = (fillData?: FillData) => number | BigNumber;
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
/** /**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
*/ */
@ -184,11 +198,11 @@ export interface GetMarketOrdersOpts {
/** /**
* Fees for each liquidity source, expressed in gas. * Fees for each liquidity source, expressed in gas.
*/ */
feeSchedule: { [source: string]: BigNumber }; feeSchedule: FeeSchedule;
/** /**
* Estimated gas consumed by each liquidity source. * Estimated gas consumed by each liquidity source.
*/ */
gasSchedule: { [source: string]: number }; gasSchedule: FeeSchedule;
/** /**
* Whether to pad the quote with a redundant fallback quote using different * Whether to pad the quote with a redundant fallback quote using different
* sources. Defaults to `true`. * sources. Defaults to `true`.
@ -210,6 +224,11 @@ export interface BatchedOperation<TResult> {
handleCallResultsAsync(contract: IERC20BridgeSamplerContract, callResults: string): Promise<TResult>; handleCallResultsAsync(contract: IERC20BridgeSamplerContract, callResults: string): Promise<TResult>;
} }
export interface SourceQuoteOperation<TFillData extends FillData = FillData> extends BatchedOperation<BigNumber[]> {
source: ERC20BridgeSource;
fillData?: TFillData;
}
/** /**
* Used in the ERC20BridgeSampler when a source does not natively * Used in the ERC20BridgeSampler when a source does not natively
* support sampling via a specific buy amount. * support sampling via a specific buy amount.

View File

@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils';
import { constants } from '../constants'; import { constants } from '../constants';
import { MarketOperation } from '../types'; import { MarketOperation } from '../types';
import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; import { CollapsedFill, FeeSchedule, OptimizedMarketOrder } from './market_operation_utils/types';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants;
@ -71,7 +71,7 @@ export interface QuoteFillInfo {
} }
export interface QuoteFillInfoOpts { export interface QuoteFillInfoOpts {
gasSchedule: { [soruce: string]: number }; gasSchedule: FeeSchedule;
protocolFeeMultiplier: BigNumber; protocolFeeMultiplier: BigNumber;
} }
@ -124,10 +124,7 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult
opts.gasSchedule, opts.gasSchedule,
), ),
// Worst case gas and protocol fee is hitting all orders. // Worst case gas and protocol fee is hitting all orders.
gas: getTotalGasUsedBySources( gas: getTotalGasUsedByFills(getFlattenedFillsFromOrders(quoteInfo.orders), opts.gasSchedule),
getFlattenedFillsFromOrders(quoteInfo.orders).map(s => s.source),
opts.gasSchedule,
),
protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length), protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length),
}; };
return fromIntermediateQuoteFillResult(result, quoteInfo); return fromIntermediateQuoteFillResult(result, quoteInfo);
@ -137,7 +134,7 @@ export function fillQuoteOrders(
fillOrders: QuoteFillOrderCall[], fillOrders: QuoteFillOrderCall[],
inputAmount: BigNumber, inputAmount: BigNumber,
protocolFeePerFillOrder: BigNumber, protocolFeePerFillOrder: BigNumber,
gasSchedule: { [source: string]: number }, gasSchedule: FeeSchedule,
): IntermediateQuoteFillResult { ): IntermediateQuoteFillResult {
const result: IntermediateQuoteFillResult = { const result: IntermediateQuoteFillResult = {
...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT,
@ -152,8 +149,9 @@ export function fillQuoteOrders(
if (remainingInput.lte(0)) { if (remainingInput.lte(0)) {
break; break;
} }
const { source } = fill; const { source, fillData } = fill;
result.gas += gasSchedule[source] || 0; const fee = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData);
result.gas += new BigNumber(fee).toNumber();
result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT;
// Actual rates are rarely linear, so fill subfills individually to // Actual rates are rarely linear, so fill subfills individually to
@ -347,10 +345,11 @@ export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): Col
return fills; return fills;
} }
function getTotalGasUsedBySources(sources: ERC20BridgeSource[], gasSchedule: { [source: string]: number }): number { function getTotalGasUsedByFills(fills: CollapsedFill[], gasSchedule: FeeSchedule): number {
let gasUsed = 0; let gasUsed = 0;
for (const s of sources) { for (const f of fills) {
gasUsed += gasSchedule[s] || 0; const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData);
gasUsed += new BigNumber(fee).toNumber();
} }
return gasUsed; return gasUsed;
} }

View File

@ -1,17 +0,0 @@
import { DEFAULT_CURVE_OPTS } from './market_operation_utils/constants';
import { ERC20BridgeSource } from './market_operation_utils/types';
export const isCurveSource = (source: ERC20BridgeSource): boolean => {
return Object.keys(DEFAULT_CURVE_OPTS).includes(source);
};
export const getCurveInfo = (
source: ERC20BridgeSource,
takerToken: string,
makerToken: string,
): { curveAddress: string; fromTokenIdx: number; toTokenIdx: number; version: number } => {
const { curveAddress, tokens, version } = DEFAULT_CURVE_OPTS[source];
const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken);
return { curveAddress, fromTokenIdx, toTokenIdx, version };
};

View File

@ -17,7 +17,7 @@ import {
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
import { isSupportedAssetDataInOrders } from './utils'; import { isSupportedAssetDataInOrders } from './utils';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
@ -126,7 +126,9 @@ export class SwapQuoteCalculator {
// Scale fees by gas price. // Scale fees by gas price.
const _opts: GetMarketOrdersOpts = { const _opts: GetMarketOrdersOpts = {
...opts, ...opts,
feeSchedule: _.mapValues(opts.feeSchedule, v => v.times(gasPrice)), feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
),
}; };
const firstOrderMakerAssetData = !!prunedOrders[0] const firstOrderMakerAssetData = !!prunedOrders[0]
@ -174,7 +176,7 @@ function createSwapQuote(
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: { [source: string]: number }, gasSchedule: FeeSchedule,
): SwapQuote { ): SwapQuote {
const bestCaseFillResult = simulateBestCaseFill({ const bestCaseFillResult = simulateBestCaseFill({
gasPrice, gasPrice,

View File

@ -12,9 +12,15 @@ import { SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils'; import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import {
BalancerPool,
computeBalancerBuyQuote,
computeBalancerSellQuote,
} from '../src/utils/market_operation_utils/balancer_utils';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockSamplerContract } from './utils/mock_sampler_contract'; import { MockSamplerContract } from './utils/mock_sampler_contract';
const CHAIN_ID = 1; const CHAIN_ID = 1;
@ -149,7 +155,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const registry = randomAddress(); const registry = randomAddress();
const sampler = new MockSamplerContract({ const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => { sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry); expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken); expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken); expect(makerToken).to.eq(expectedMakerToken);
@ -158,12 +164,13 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes( await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.LiquidityProvider], [ERC20BridgeSource.LiquidityProvider],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
registry, registry,
), ),
); );
@ -173,6 +180,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider', source: 'LiquidityProvider',
output: toBaseUnitAmount(1001), output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined,
}, },
], ],
]); ]);
@ -183,7 +191,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const registry = randomAddress(); const registry = randomAddress();
const sampler = new MockSamplerContract({ const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => { sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry); expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken); expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken); expect(makerToken).to.eq(expectedMakerToken);
@ -192,12 +200,13 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getBuyQuotes( await DexOrderSampler.ops.getBuyQuotesAsync(
[ERC20BridgeSource.LiquidityProvider], [ERC20BridgeSource.LiquidityProvider],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
registry, registry,
), ),
); );
@ -207,6 +216,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider', source: 'LiquidityProvider',
output: toBaseUnitAmount(999), output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined,
}, },
], ],
]); ]);
@ -233,12 +243,13 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes( await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.MultiBridge], [ERC20BridgeSource.MultiBridge],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
randomAddress(), randomAddress(),
dexOrderSampler.balancerPoolsCache,
randomAddress(), randomAddress(),
multiBridge, multiBridge,
), ),
@ -249,6 +260,7 @@ describe('DexSampler tests', () => {
source: 'MultiBridge', source: 'MultiBridge',
output: toBaseUnitAmount(1001), output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined,
}, },
], ],
]); ]);
@ -412,72 +424,92 @@ describe('DexSampler tests', () => {
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
}, },
sampleSellsFromUniswapV2: (path, fillAmounts) => { sampleSellsFromUniswapV2: (path, fillAmounts) => {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
} else {
expect(path).to.have.lengthOf.within(2, 3);
}
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue()); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes( await DexOrderSampler.ops.getSellQuotesAsync(
sources, sources,
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedTakerFillAmounts, expectedTakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s => const expectedQuotes = sources.map(s =>
expectedTakerFillAmounts.map(a => ({ expectedTakerFillAmounts.map(a => ({
source: s, source: s,
input: a, input: a,
output: a.times(ratesBySource[s]).integerValue(), output: a.times(ratesBySource[s]).integerValue(),
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
})), })),
); );
expect(quotes).to.deep.eq(expectedQuotes); const uniswapV2ETHQuotes = [
expectedTakerFillAmounts.map(a => ({
source: ERC20BridgeSource.UniswapV2,
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
];
// extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB)
expect(quotes).to.have.lengthOf(sources.length + 1);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
}); });
it('getSellQuotes() uses samples from Balancer', async () => {
it('getSellQuotes() includes ETH for Uniswap_V2_ETH', async () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.UniswapV2Eth];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.UniswapV2Eth]: getRandomFloat(0, 100),
};
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const sampler = new MockSamplerContract({ const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
sampleSellsFromUniswapV2: (path, fillAmounts) => { const balancerPoolsCache = new MockBalancerPoolsCache({
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]); getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); expect(takerToken).equal(expectedTakerToken);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2Eth]).integerValue()); expect(makerToken).equal(expectedMakerToken);
return Promise.resolve(pools);
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(new MockSamplerContract({}), balancerPoolsCache);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes( await DexOrderSampler.ops.getSellQuotesAsync(
sources, [ERC20BridgeSource.Balancer],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedTakerFillAmounts, expectedTakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
expect(quotes).to.be.length(sources.length); const expectedQuotes = pools.map(p =>
const expectedQuotes = sources.map(s =>
expectedTakerFillAmounts.map(a => ({ expectedTakerFillAmounts.map(a => ({
source: s, source: ERC20BridgeSource.Balancer,
input: a, input: a,
output: a.times(ratesBySource[s]).integerValue(), output: computeBalancerSellQuote(p, a),
fillData: { poolAddress: p.id },
})), })),
); );
expect(quotes).to.have.lengthOf(2); // one array per pool
expect(quotes).to.deep.eq(expectedQuotes); expect(quotes).to.deep.eq(expectedQuotes);
}); });
it('getBuyQuotes()', async () => { it('getBuyQuotes()', async () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap]; const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
const ratesBySource: RatesBySource = { const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100), [ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100),
[ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100), [ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100),
@ -498,78 +530,85 @@ describe('DexSampler tests', () => {
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
}, },
sampleBuysFromUniswapV2: (path, fillAmounts) => { sampleBuysFromUniswapV2: (path, fillAmounts) => {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
} else {
expect(path).to.have.lengthOf.within(2, 3);
}
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue()); return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getBuyQuotes( await DexOrderSampler.ops.getBuyQuotesAsync(
sources, sources,
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedMakerFillAmounts, expectedMakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s => const expectedQuotes = sources.map(s =>
expectedMakerFillAmounts.map(a => ({ expectedMakerFillAmounts.map(a => ({
source: s, source: s,
input: a, input: a,
output: a.times(ratesBySource[s]).integerValue(), output: a.times(ratesBySource[s]).integerValue(),
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
})), })),
); );
expect(quotes).to.deep.eq(expectedQuotes); const uniswapV2ETHQuotes = [
expectedMakerFillAmounts.map(a => ({
source: ERC20BridgeSource.UniswapV2,
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
];
// extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB)
expect(quotes).to.have.lengthOf(sources.length + 1);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
}); });
it('getBuyQuotes() includes ETH for Uniswap_V2_ETH', async () => { it('getBuyQuotes() uses samples from Balancer', async () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2Eth];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100),
[ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100),
[ERC20BridgeSource.UniswapV2Eth]: getRandomFloat(0, 100),
};
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const sampler = new MockSamplerContract({ const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => { const balancerPoolsCache = new MockBalancerPoolsCache({
expect(takerToken).to.eq(expectedTakerToken); getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(makerToken).to.eq(expectedMakerToken); expect(takerToken).equal(expectedTakerToken);
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); expect(makerToken).equal(expectedMakerToken);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue()); return Promise.resolve(pools);
},
sampleBuysFromEth2Dai: (takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
},
sampleBuysFromUniswapV2: (path, fillAmounts) => {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2Eth]).integerValue());
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(new MockSamplerContract({}), balancerPoolsCache);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getBuyQuotes( await DexOrderSampler.ops.getBuyQuotesAsync(
sources, [ERC20BridgeSource.Balancer],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedMakerFillAmounts, expectedMakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
expect(quotes).to.be.length(sources.length); const expectedQuotes = pools.map(p =>
const expectedQuotes = sources.map(s =>
expectedMakerFillAmounts.map(a => ({ expectedMakerFillAmounts.map(a => ({
source: s, source: ERC20BridgeSource.Balancer,
input: a, input: a,
output: a.times(ratesBySource[s]).integerValue(), output: computeBalancerBuyQuote(p, a),
fillData: { poolAddress: p.id },
})), })),
); );
expect(quotes).to.have.lengthOf(2); // one set per pool
expect(quotes).to.deep.eq(expectedQuotes); expect(quotes).to.deep.eq(expectedQuotes);
}); });
}); });
@ -600,4 +639,14 @@ describe('DexSampler tests', () => {
}); });
}); });
}); });
function generateBalancerPool(): BalancerPool {
return {
id: randomAddress(),
balanceIn: getRandomInteger(1, 1e18),
balanceOut: getRandomInteger(1, 1e18),
weightIn: getRandomInteger(0, 1e5),
weightOut: getRandomInteger(0, 1e5),
swapFee: getRandomInteger(0, 1e5),
};
}
// tslint:disable-next-line: max-file-line-count // tslint:disable-next-line: max-file-line-count

View File

@ -16,16 +16,10 @@ import * as _ from 'lodash';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../src';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants';
BUY_SOURCES,
DEFAULT_CURVE_OPTS,
POSITIVE_INF,
SELL_SOURCES,
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, NativeFillData } from '../src/utils/market_operation_utils/types'; import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types';
// tslint:disable: custom-no-magic-numbers // tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
@ -93,10 +87,7 @@ describe('MarketOperationUtils tests', () => {
case UNISWAP_V2_BRIDGE_ADDRESS.toLowerCase(): case UNISWAP_V2_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.UniswapV2; return ERC20BridgeSource.UniswapV2;
case CURVE_BRIDGE_ADDRESS.toLowerCase(): case CURVE_BRIDGE_ADDRESS.toLowerCase():
const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter( return ERC20BridgeSource.Curve;
k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
);
return curveSource[0] as ERC20BridgeSource;
default: default:
break; break;
} }
@ -132,12 +123,18 @@ describe('MarketOperationUtils tests', () => {
chainId: CHAIN_ID, chainId: CHAIN_ID,
}; };
function createSamplesFromRates(source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[]): DexSample[] { function createSamplesFromRates(
source: ERC20BridgeSource,
inputs: Numberish[],
rates: Numberish[],
fillData?: FillData,
): DexSample[] {
const samples: DexSample[] = []; const samples: DexSample[] = [];
inputs.forEach((input, i) => { inputs.forEach((input, i) => {
const rate = rates[i]; const rate = rates[i];
samples.push({ samples.push({
source, source,
fillData: fillData || DEFAULT_FILL_DATA[source],
input: new BigNumber(input), input: new BigNumber(input),
output: new BigNumber(input) output: new BigNumber(input)
.minus(i === 0 ? 0 : samples[i - 1].input) .minus(i === 0 ? 0 : samples[i - 1].input)
@ -161,10 +158,10 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return ( return (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, _makerToken: string,
takerToken: string, _takerToken: string,
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
wethAddress: string, _wethAddress: string,
) => { ) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s])); return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
}; };
@ -184,10 +181,11 @@ describe('MarketOperationUtils tests', () => {
takerToken: string, takerToken: string,
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
wethAddress: string, wethAddress: string,
_balancerPoolsCache?: any,
liquidityProviderAddress?: string, liquidityProviderAddress?: string,
) => { ) => {
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress; liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
liquidityPoolParams.sources = sources; liquidityPoolParams.sources = liquidityPoolParams.sources.concat(sources);
return tradeOperation(rates)( return tradeOperation(rates)(
sources, sources,
makerToken, makerToken,
@ -203,10 +201,10 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return ( return (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, _makerToken: string,
takerToken: string, _takerToken: string,
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 sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
}; };
@ -229,18 +227,18 @@ describe('MarketOperationUtils tests', () => {
function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation {
return ( return (
sources: ERC20BridgeSource[], _sources: ERC20BridgeSource[],
makerToken: string, _makerToken: string,
takerToken: string, _takerToken: string,
fillAmounts: BigNumber[], _fillAmounts: BigNumber[],
wethAddress: string, _wethAddress: string,
) => { ) => {
return new BigNumber(rate); return new BigNumber(rate);
}; };
} }
function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation { function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation {
return (registryAddress: string, takerToken: string, makerToken: string): string => { return (_registryAddress: string, _takerToken: string, _makerToken: string): string => {
return NULL_ADDRESS; return NULL_ADDRESS;
}; };
} }
@ -288,17 +286,23 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.UniswapV2]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.UniswapV2]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.UniswapV2Eth]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Balancer]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDai]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Curve]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdt]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.MultiBridge]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.MultiBridge]: _.times(NUM_SAMPLES, () => 0),
}; };
interface FillDataBySource {
[source: string]: FillData;
}
const DEFAULT_FILL_DATA: FillDataBySource = {
[ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] },
[ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() },
[ERC20BridgeSource.Curve]: { poolAddress: randomAddress(), fromTokenIdx: 0, toTokenIdx: 1 },
};
const DEFAULT_OPS = { const DEFAULT_OPS = {
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount); return orders.map(o => o.takerAssetAmount);
@ -306,9 +310,9 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount); return orders.map(o => o.makerAssetAmount);
}, },
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1), getMedianSellRateAsync: createGetMedianSellRate(1),
getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(), getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(),
}; };
@ -346,11 +350,7 @@ describe('MarketOperationUtils tests', () => {
sampleDistributionBase: 1, sampleDistributionBase: 1,
bridgeSlippage: 0, bridgeSlippage: 0,
maxFallbackSlippage: 100, maxFallbackSlippage: 100,
excludedSources: [ excludedSources: [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer],
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2Eth,
...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]),
],
allowFallback: false, allowFallback: false,
shouldBatchBridgeOrders: false, shouldBatchBridgeOrders: false,
}; };
@ -363,9 +363,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, NUM_SAMPLES); const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0; let actualNumSamples = 0;
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length; actualNumSamples = amounts.length;
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -378,9 +378,9 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice(); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -396,7 +396,7 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES, DEFAULT_RATES,
); );
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: fn, getSellQuotesAsync: fn,
}); });
const registryAddress = randomAddress(); const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils( const newMarketOperationUtils = new MarketOperationUtils(
@ -419,9 +419,9 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice(); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -477,7 +477,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const expectedMakerAmount = order.fills[0].output; const expectedMakerAmount = order.fills[0].output;
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); const slippage = new BigNumber(1).minus(order.makerAssetAmount.div(expectedMakerAmount.plus(1)));
assertRoughlyEquals(slippage, bridgeSlippage, 1); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
}); });
@ -485,11 +485,11 @@ describe('MarketOperationUtils tests', () => {
it('can mix convex sources', async () => { it('can mix convex sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.UniswapV2] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -499,7 +499,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
@ -514,18 +514,20 @@ describe('MarketOperationUtils tests', () => {
const nativeFeeRate = 0.06; const nativeFeeRate = 0.06;
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91] [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91]
[ERC20BridgeSource.UniswapV2]: [0.96, 0.1, 0.1, 0.1], [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: _.constant(
.times(nativeFeeRate) FILL_AMOUNT.div(4)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -535,7 +537,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
@ -551,16 +553,18 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0] // Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.UniswapV2]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: _.constant(
.times(uniswapFeeRate) FILL_AMOUNT.div(4)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -571,7 +575,7 @@ describe('MarketOperationUtils tests', () => {
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
]; ];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
@ -580,12 +584,12 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use [ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use
[ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave [ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave
[ERC20BridgeSource.UniswapV2]: [0.96, 0.2, 0.1, 0.1], [ERC20BridgeSource.Uniswap]: [0.96, 0.2, 0.1, 0.1],
[ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1], [ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1],
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -595,7 +599,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
@ -604,11 +608,11 @@ describe('MarketOperationUtils tests', () => {
it('fallback orders use different sources', async () => { it('fallback orders use different sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
rates[ERC20BridgeSource.UniswapV2] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -620,7 +624,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
]; ];
const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber]; const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
@ -630,11 +634,11 @@ describe('MarketOperationUtils tests', () => {
it('does not create a fallback if below maxFallbackSlippage', async () => { it('does not create a fallback if below maxFallbackSlippage', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.UniswapV2] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -642,7 +646,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.UniswapV2]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
@ -667,7 +671,7 @@ describe('MarketOperationUtils tests', () => {
] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress); ] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress);
replaceSamplerOps({ replaceSamplerOps({
getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT],
getSellQuotes: getSellQuotesFn, getSellQuotesAsync: getSellQuotesFn,
getLiquidityProviderFromRegistry: getLiquidityProviderFn, getLiquidityProviderFromRegistry: getLiquidityProviderFn,
}); });
@ -706,12 +710,12 @@ describe('MarketOperationUtils tests', () => {
it('batches contiguous bridge sources', async () => { it('batches contiguous bridge sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.UniswapV2] = [1, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.CurveUsdcDai] = [0.48, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -721,7 +725,7 @@ describe('MarketOperationUtils tests', () => {
numSamples: 4, numSamples: 4,
excludedSources: [ excludedSources: [
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai), ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve),
], ],
shouldBatchBridgeOrders: true, shouldBatchBridgeOrders: true,
}, },
@ -729,9 +733,9 @@ describe('MarketOperationUtils tests', () => {
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 = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.UniswapV2], [ERC20BridgeSource.Uniswap],
[ERC20BridgeSource.Native], [ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.CurveUsdcDai], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve],
]); ]);
}); });
}); });
@ -748,10 +752,10 @@ describe('MarketOperationUtils tests', () => {
bridgeSlippage: 0, bridgeSlippage: 0,
maxFallbackSlippage: 100, maxFallbackSlippage: 100,
excludedSources: [ excludedSources: [
...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]),
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.UniswapV2Eth, ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer,
], ],
allowFallback: false, allowFallback: false,
shouldBatchBridgeOrders: false, shouldBatchBridgeOrders: false,
@ -765,9 +769,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, 16); const numSamples = _.random(1, 16);
let actualNumSamples = 0; let actualNumSamples = 0;
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length; actualNumSamples = amounts.length;
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -780,9 +784,9 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice(); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -798,7 +802,7 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES, DEFAULT_RATES,
); );
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: fn, getBuyQuotesAsync: fn,
}); });
const registryAddress = randomAddress(); const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils( const newMarketOperationUtils = new MarketOperationUtils(
@ -821,9 +825,9 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice(); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -879,7 +883,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const expectedTakerAmount = order.fills[0].output; const expectedTakerAmount = order.fills[0].output;
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).minus(1);
assertRoughlyEquals(slippage, bridgeSlippage, 1); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
}); });
@ -887,10 +891,10 @@ describe('MarketOperationUtils tests', () => {
it('can mix convex sources', async () => { it('can mix convex sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.UniswapV2] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -900,7 +904,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
@ -915,18 +919,20 @@ describe('MarketOperationUtils tests', () => {
const nativeFeeRate = 0.06; const nativeFeeRate = 0.06;
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91] [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91]
[ERC20BridgeSource.UniswapV2]: [0.96, 0.1, 0.1, 0.1], [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: _.constant(
.times(nativeFeeRate) FILL_AMOUNT.div(4)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -935,7 +941,7 @@ describe('MarketOperationUtils tests', () => {
); );
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -950,17 +956,19 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0] // Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.UniswapV2]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.UniswapV2]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: _.constant(
.times(uniswapFeeRate) FILL_AMOUNT.div(4)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -971,7 +979,7 @@ describe('MarketOperationUtils tests', () => {
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
]; ];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
@ -979,10 +987,10 @@ describe('MarketOperationUtils tests', () => {
it('fallback orders use different sources', async () => { it('fallback orders use different sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
rates[ERC20BridgeSource.UniswapV2] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -994,7 +1002,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Uniswap,
]; ];
const secondSources = [ERC20BridgeSource.Eth2Dai]; const secondSources = [ERC20BridgeSource.Eth2Dai];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
@ -1004,10 +1012,10 @@ describe('MarketOperationUtils tests', () => {
it('does not create a fallback if below maxFallbackSlippage', async () => { it('does not create a fallback if below maxFallbackSlippage', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.UniswapV2] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1015,7 +1023,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.UniswapV2]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
@ -1025,9 +1033,9 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.UniswapV2] = [0.48, 0.47, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1042,7 +1050,7 @@ describe('MarketOperationUtils tests', () => {
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Native], [ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.UniswapV2], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
]); ]);
}); });
}); });
@ -1078,7 +1086,7 @@ describe('MarketOperationUtils tests', () => {
}; };
const orders = [smallOrder, largeOrder]; const orders = [smallOrder, largeOrder];
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: new BigNumber(2e5), [ERC20BridgeSource.Native]: _.constant(2e5),
}; };
it('penalizes native fill based on target amount when target is smaller', () => { it('penalizes native fill based on target amount when target is smaller', () => {

View File

@ -22,7 +22,7 @@ describe('quote_simulation tests', async () => {
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: _.constant(1) };
// Check if two numbers are within `maxError` error rate within each other (default 1 bps). // Check if two numbers are within `maxError` error rate within each other (default 1 bps).
function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-12): void { function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-12): void {

View File

@ -0,0 +1,24 @@
import { BalancerPool, BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils';
export interface Handlers {
getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>;
_fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>;
}
export class MockBalancerPoolsCache extends BalancerPoolsCache {
constructor(public handlers: Partial<Handlers>) {
super();
}
public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
return this.handlers.getPoolsForPairAsync
? this.handlers.getPoolsForPairAsync(takerToken, makerToken)
: super.getPoolsForPairAsync(takerToken, makerToken);
}
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
return this.handlers._fetchPoolsForPairAsync
? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken)
: super._fetchPoolsForPairAsync(takerToken, makerToken);
}
}

View File

@ -226,6 +226,9 @@ export class MockSamplerContract extends IERC20BridgeSamplerContract {
} }
private _callEncodedFunction(callData: string): string { private _callEncodedFunction(callData: string): string {
if (callData === '0x') {
return callData;
}
// tslint:disable-next-line: custom-no-magic-numbers // tslint:disable-next-line: custom-no-magic-numbers
const selector = hexUtils.slice(callData, 0, 4); const selector = hexUtils.slice(callData, 0, 4);
for (const [name, handler] of Object.entries(this._handlers)) { for (const [name, handler] of Object.entries(this._handlers)) {

View File

@ -25,6 +25,10 @@
{ {
"note": "Update ganache snapshot Exchange Proxy addresses for MetaTransactions", "note": "Update ganache snapshot Exchange Proxy addresses for MetaTransactions",
"pr": 2610 "pr": 2610
},
{
"note": "Add BalancerBridge addresses",
"pr": 2613
} }
] ]
}, },

View File

@ -33,6 +33,7 @@
"maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf",
"dexForwarderBridge": "0xc47b7094f378e54347e281aab170e8cca69d880a", "dexForwarderBridge": "0xc47b7094f378e54347e281aab170e8cca69d880a",
"multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1", "multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1",
"balancerBridge": "0xfe01821ca163844203220cd08e4f2b2fb43ae4e4",
"exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e",
"exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb",
@ -79,6 +80,7 @@
"maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a",
"dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1",
"multiBridge": "0x0000000000000000000000000000000000000000", "multiBridge": "0x0000000000000000000000000000000000000000",
"balancerBridge": "0x47697b44bd89051e93b4d5857ba8e024800a74ac",
"exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e",
"exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb",
@ -125,6 +127,7 @@
"maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac",
"dexForwarderBridge": "0x0000000000000000000000000000000000000000", "dexForwarderBridge": "0x0000000000000000000000000000000000000000",
"multiBridge": "0x0000000000000000000000000000000000000000", "multiBridge": "0x0000000000000000000000000000000000000000",
"balancerBridge": "0x5d8c9ba74607d2cbc4176882a42d4ace891c1c00",
"exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e",
"exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb",
@ -171,6 +174,7 @@
"maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74", "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74",
"dexForwarderBridge": "0xf220eb0b29e18bbc8ebc964e915b7547c7b4de4f", "dexForwarderBridge": "0xf220eb0b29e18bbc8ebc964e915b7547c7b4de4f",
"multiBridge": "0x0000000000000000000000000000000000000000", "multiBridge": "0x0000000000000000000000000000000000000000",
"balancerBridge": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a",
"exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e",
"exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb",
@ -217,6 +221,7 @@
"maximumGasPrice": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x0000000000000000000000000000000000000000",
"dexForwarderBridge": "0x0000000000000000000000000000000000000000", "dexForwarderBridge": "0x0000000000000000000000000000000000000000",
"multiBridge": "0x0000000000000000000000000000000000000000", "multiBridge": "0x0000000000000000000000000000000000000000",
"balancerBridge": "0x0000000000000000000000000000000000000000",
"exchangeProxyGovernor": "0x0000000000000000000000000000000000000000", "exchangeProxyGovernor": "0x0000000000000000000000000000000000000000",
"exchangeProxy": "0x2ebb94cc79d7d0f1195300aaf191d118f53292a8", "exchangeProxy": "0x2ebb94cc79d7d0f1195300aaf191d118f53292a8",
"exchangeProxyAllowanceTarget": "0x3eab3df72fd584b50184ff7d988a0d8f9328c866", "exchangeProxyAllowanceTarget": "0x3eab3df72fd584b50184ff7d988a0d8f9328c866",

View File

@ -34,6 +34,7 @@ export interface ContractAddresses {
maximumGasPrice: string; maximumGasPrice: string;
dexForwarderBridge: string; dexForwarderBridge: string;
multiBridge: string; multiBridge: string;
balancerBridge: string;
exchangeProxyGovernor: string; exchangeProxyGovernor: string;
exchangeProxy: string; exchangeProxy: string;
exchangeProxyAllowanceTarget: string; exchangeProxyAllowanceTarget: string;

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,10 @@
{ {
"note": "Add affiliate fee transformer migration and flash wallet address", "note": "Add affiliate fee transformer migration and flash wallet address",
"pr": 2622 "pr": 2622
},
{
"note": "Add BalancerBridge to returned object in `migration.ts`",
"pr": 2613
} }
] ]
}, },

View File

@ -379,6 +379,7 @@ export async function runMigrationsAsync(
maximumGasPrice: NULL_ADDRESS, maximumGasPrice: NULL_ADDRESS,
dexForwarderBridge: NULL_ADDRESS, dexForwarderBridge: NULL_ADDRESS,
multiBridge: NULL_ADDRESS, multiBridge: NULL_ADDRESS,
balancerBridge: NULL_ADDRESS,
exchangeProxyGovernor: NULL_ADDRESS, exchangeProxyGovernor: NULL_ADDRESS,
exchangeProxy: exchangeProxy.address, exchangeProxy: exchangeProxy.address,
exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress,

View File

@ -9,6 +9,10 @@
{ {
"note": "Add `Set` to `EXTERNAL_TYPE_MAP`.", "note": "Add `Set` to `EXTERNAL_TYPE_MAP`.",
"pr": 2350 "pr": 2350
},
{
"note": "Add `TFillData` to `EXTERNAL_TYPE_MAP`",
"pr": 2613
} }
] ]
}, },

View File

@ -24,6 +24,8 @@ export const docGenConfigs: DocGenConfigs = {
// HACK: Asset-swapper specifies marketSell and marketBuy quotes with a descriminant MarketOperation Type to ignore the error, linking Buy and Sell to MarketOperation // HACK: Asset-swapper specifies marketSell and marketBuy quotes with a descriminant MarketOperation Type to ignore the error, linking Buy and Sell to MarketOperation
Buy: true, Buy: true,
Sell: true, Sell: true,
// HACK: Asset-swapper specifies TFillData as any type that extends FillData
TFillData: true,
IterableIterator: true, IterableIterator: true,
Set: true, Set: true,
}, },

View File

@ -1,4 +1,13 @@
[ [
{
"version": "4.1.0",
"changes": [
{
"note": "Set `no-non-null-assertion` to false",
"pr": 2613
}
]
},
{ {
"version": "4.0.0", "version": "4.0.0",
"changes": [ "changes": [

View File

@ -65,7 +65,7 @@
"no-lodash-isnull": true, "no-lodash-isnull": true,
"no-lodash-isundefined": true, "no-lodash-isundefined": true,
"no-misused-new": true, "no-misused-new": true,
"no-non-null-assertion": true, "no-non-null-assertion": false,
"no-parameter-reassignment": true, "no-parameter-reassignment": true,
"no-redundant-jsdoc": true, "no-redundant-jsdoc": true,
"no-return-await": true, "no-return-await": true,

View File

@ -34,4 +34,9 @@ if (isNode) {
}; };
} }
// HACK: CLobber config and set to prevent imported packages from poisoning
// global BigNumber config
(orig => (BigNumber.config = (..._args: any[]) => orig({})))(BigNumber.config);
BigNumber.set = BigNumber.config;
export { BigNumber }; export { BigNumber };

View File

@ -1047,6 +1047,16 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@balancer-labs/sor@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-0.3.0.tgz#c221225d9a3d1791ebfc3c566f7a76843bca98fa"
integrity sha512-QTVkeDmcGCaEgBhcVSu8c7cz6HA1ueWRbniuT+Yh0N/sqcZIcDMdoCFcpq66SD+hOxQ88RvzShmJ+P/3vKbXfg==
dependencies:
bignumber.js "^9.0.0"
ethers "^4.0.39"
isomorphic-fetch "^2.2.1"
typescript "^3.8.3"
"@cnakazawa/watch@^1.0.3": "@cnakazawa/watch@^1.0.3":
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@ -3906,7 +3916,7 @@ big.js@^5.2.2:
version "5.2.2" version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
bignumber.js@*, bignumber.js@~9.0.0: bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@~9.0.0:
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
@ -6283,6 +6293,19 @@ elliptic@6.3.3:
hash.js "^1.0.0" hash.js "^1.0.0"
inherits "^2.0.1" inherits "^2.0.1"
elliptic@6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0: elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0:
version "6.4.0" version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@ -7115,6 +7138,21 @@ ethers@4.0.0-beta.3:
uuid "2.0.1" uuid "2.0.1"
xmlhttprequest "1.8.0" xmlhttprequest "1.8.0"
ethers@^4.0.39:
version "4.0.47"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.47.tgz#91b9cd80473b1136dd547095ff9171bd1fc68c85"
integrity sha512-hssRYhngV4hiDNeZmVU/k5/E8xmLG8UpcNUzg6mb7lqhgpFPH/t7nuv20RjRrEf0gblzvi2XwR5Te+V3ZFc9pQ==
dependencies:
aes-js "3.0.0"
bn.js "^4.4.0"
elliptic "6.5.2"
hash.js "1.1.3"
js-sha3 "0.5.7"
scrypt-js "2.0.4"
setimmediate "1.0.4"
uuid "2.0.1"
xmlhttprequest "1.8.0"
ethers@~4.0.4: ethers@~4.0.4:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65"
@ -9771,7 +9809,7 @@ isobject@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1: isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies: dependencies:
@ -16802,6 +16840,11 @@ typescript@3.5.x:
version "3.5.3" version "3.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
typescript@^3.8.3:
version "3.9.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
typewise-core@^1.2, typewise-core@^1.2.0: typewise-core@^1.2, typewise-core@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195"