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": [
{
"note": "Fix instability with DFB.",
"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"
},
"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."
},
"repository": {

View File

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

View File

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

View File

@ -3,6 +3,7 @@
* 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/curve_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_dispatcher';
export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_balancer_pool';
export * from '../generated-wrappers/i_chai';
export * from '../generated-wrappers/i_curve';
export * from '../generated-wrappers/i_dydx';

View File

@ -5,6 +5,7 @@
*/
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 CurveBridge from '../test/generated-artifacts/CurveBridge.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 IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyDispatcher.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 ICurve from '../test/generated-artifacts/ICurve.json';
import * as IDydx from '../test/generated-artifacts/IDydx.json';
@ -57,6 +59,7 @@ export const artifacts = {
ERC721Proxy: ERC721Proxy as ContractArtifact,
MultiAssetProxy: MultiAssetProxy as ContractArtifact,
StaticCallProxy: StaticCallProxy as ContractArtifact,
BalancerBridge: BalancerBridge as ContractArtifact,
ChaiBridge: ChaiBridge as ContractArtifact,
CurveBridge: CurveBridge as ContractArtifact,
DexForwarderBridge: DexForwarderBridge as ContractArtifact,
@ -70,6 +73,7 @@ export const artifacts = {
IAssetProxy: IAssetProxy as ContractArtifact,
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact,
IBalancerPool: IBalancerPool as ContractArtifact,
IChai: IChai as ContractArtifact,
ICurve: ICurve 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.
* -----------------------------------------------------------------------------
*/
export * from '../test/generated-wrappers/balancer_bridge';
export * from '../test/generated-wrappers/chai_bridge';
export * from '../test/generated-wrappers/curve_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_dispatcher';
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_curve';
export * from '../test/generated-wrappers/i_dydx';

View File

@ -3,6 +3,7 @@
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [
"generated-artifacts/BalancerBridge.json",
"generated-artifacts/ChaiBridge.json",
"generated-artifacts/CurveBridge.json",
"generated-artifacts/DexForwarderBridge.json",
@ -16,6 +17,7 @@
"generated-artifacts/IAssetProxy.json",
"generated-artifacts/IAssetProxyDispatcher.json",
"generated-artifacts/IAuthorizable.json",
"generated-artifacts/IBalancerPool.json",
"generated-artifacts/IChai.json",
"generated-artifacts/ICurve.json",
"generated-artifacts/IDydx.json",
@ -45,6 +47,7 @@
"generated-artifacts/TestUniswapV2Bridge.json",
"generated-artifacts/UniswapBridge.json",
"generated-artifacts/UniswapV2Bridge.json",
"test/generated-artifacts/BalancerBridge.json",
"test/generated-artifacts/ChaiBridge.json",
"test/generated-artifacts/CurveBridge.json",
"test/generated-artifacts/DexForwarderBridge.json",
@ -58,6 +61,7 @@
"test/generated-artifacts/IAssetProxy.json",
"test/generated-artifacts/IAssetProxyDispatcher.json",
"test/generated-artifacts/IAuthorizable.json",
"test/generated-artifacts/IBalancerPool.json",
"test/generated-artifacts/IChai.json",
"test/generated-artifacts/ICurve.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",
"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.",
"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/utils": "^5.5.0",
"@0x/web3-wrapper": "^7.1.0",
"@balancer-labs/sor": "^0.3.0",
"axios": "^0.19.2",
"axios-mock-adapter": "^1.18.1",
"decimal.js": "^10.2.0",
"ethereumjs-util": "^5.1.1",
"heartbeats": "^5.0.1",
"lodash": "^4.17.11"

View File

@ -69,6 +69,12 @@ export {
NativeCollapsedFill,
OptimizedMarketOrder,
GetMarketOrdersRfqtOpts,
FeeSchedule,
FillData,
NativeFillData,
CurveFillData,
BalancerFillData,
UniswapV2FillData,
} from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_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 = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.UniswapV2Eth,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
// All Curve Sources
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
ERC20BridgeSource.CurveUsdcDaiUsdtBusd,
ERC20BridgeSource.CurveUsdcDaiUsdtSusd,
ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer,
];
/**
@ -27,15 +22,10 @@ export const SELL_SOURCES = [
export const BUY_SOURCES = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.UniswapV2Eth,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
// All Curve sources
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtBusd,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
ERC20BridgeSource.CurveUsdcDaiUsdtSusd,
ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer,
];
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.
*/
export const FEE_QUOTE_SOURCES = SELL_SOURCES;
export const FEE_QUOTE_SOURCES = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
];
/**
* Mainnet Curve configuration
*/
export const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
[ERC20BridgeSource.CurveUsdcDai]: {
version: 1,
curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
},
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
version: 1,
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
version: 1,
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x0000000000085d4780b73119b644ae5ecd22b376',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: {
version: 1,
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: {
version: 1,
curveAddress: '0xa5407eae9ba41422680e2e00537571bcc53efbfd',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x57ab1ec28d129707052df4df418d58a2d46d5f51',
],
},
export const MAINNET_CURVE_CONTRACTS: { [curveAddress: string]: string[] } = {
'0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56': [
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
],
'0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c': [
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
],
'0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51': [
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0x0000000000085d4780b73119b644ae5ecd22b376', // TUSD
],
'0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27': [
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0x4fabb145d64652a948d72533023f6e7a623c7c53', // BUSD
],
'0xa5407eae9ba41422680e2e00537571bcc53efbfd': [
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0x57ab1ec28d129707052df4df418d58a2d46d5f51', // SUSD
],
};
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 { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
Fill,
FillFlags,
NativeCollapsedFill,
NativeFillData,
} from './types';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
@ -26,7 +18,7 @@ export function createFillPaths(opts: {
targetInput?: BigNumber;
ethToOutputRate?: BigNumber;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: { [source: string]: BigNumber };
feeSchedule?: FeeSchedule;
}): Fill[][] {
const { side } = opts;
const excludedSources = opts.excludedSources || [];
@ -62,7 +54,7 @@ function nativeOrdersToPath(
orders: SignedOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF,
ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber },
fees: FeeSchedule,
): Fill[] {
// Create a single path from all orders.
let path: Fill[] = [];
@ -71,7 +63,9 @@ function nativeOrdersToPath(
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
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);
// targetInput can be less than the order size
// whilst the penalty is constant, it affects the adjusted output
@ -116,7 +110,7 @@ function dexQuotesToPaths(
side: MarketOperation,
dexQuotes: DexSample[][],
ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber },
fees: FeeSchedule,
): Fill[][] {
const paths: Fill[][] = [];
for (let quote of dexQuotes) {
@ -129,12 +123,13 @@ function dexQuotesToPaths(
for (let i = 0; i < quote.length; i++) {
const sample = quote[i];
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 output = sample.output.minus(prevSample ? prevSample.output : 0);
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData);
const penalty =
i === 0 // Only the first fill in a DEX path incurs a penalty.
? ethToOutputRate.times(fees[source] || 0)
? ethToOutputRate.times(fee)
: ZERO_AMOUNT;
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output);
@ -147,6 +142,7 @@ function dexQuotesToPaths(
adjustedRate,
adjustedOutput,
source,
fillData,
index: i,
parent: i !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(source),
@ -241,7 +237,7 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_
}
export function collapsePath(path: Fill[]): CollapsedFill[] {
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
const collapsed: CollapsedFill[] = [];
for (const fill of path) {
const source = fill.source;
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
@ -256,10 +252,10 @@ export function collapsePath(path: Fill[]): CollapsedFill[] {
}
collapsed.push({
source: fill.source,
fillData: fill.fillData,
input: fill.input,
output: fill.output,
subFills: [fill],
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
});
}
return collapsed;

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import { ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { getCurveInfo } from '../source_utils';
import {
ERC20_PROXY_ID,
@ -20,12 +19,15 @@ import { collapsePath } from './fills';
import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import {
AggregationError,
BalancerFillData,
CollapsedFill,
CurveFillData,
ERC20BridgeSource,
Fill,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
UniswapV2FillData,
} from './types';
// tslint:disable completed-docs no-unnecessary-type-assertion
@ -151,7 +153,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
const orders: OptimizedMarketOrder[] = [];
for (let i = 0; i < collapsedPath.length; ) {
if (collapsedPath[i].source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(collapsedPath[i]));
orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill));
++i;
continue;
}
@ -184,14 +186,11 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder
case ERC20BridgeSource.Uniswap:
return opts.contractAddresses.uniswapBridge;
case ERC20BridgeSource.UniswapV2:
case ERC20BridgeSource.UniswapV2Eth:
return opts.contractAddresses.uniswapV2Bridge;
case ERC20BridgeSource.CurveUsdcDai:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtSusd:
case ERC20BridgeSource.Curve:
return opts.contractAddresses.curveBridge;
case ERC20BridgeSource.Balancer:
return opts.contractAddresses.balancerBridge;
case ERC20BridgeSource.LiquidityProvider:
if (opts.liquidityProviderAddress === undefined) {
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;
switch (fill.source) {
case ERC20BridgeSource.CurveUsdcDai:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtSusd:
const { curveAddress, fromTokenIdx, toTokenIdx, version } = getCurveInfo(
fill.source,
takerToken,
makerToken,
);
case ERC20BridgeSource.Curve:
const curveFillData = (fill as CollapsedFill<CurveFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
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;
case ERC20BridgeSource.UniswapV2:
const uniswapV2FillData = (fill as CollapsedFill<UniswapV2FillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createUniswapV2BridgeData([takerToken, makerToken]),
);
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]),
createUniswapV2BridgeData(uniswapV2FillData.tokenAddressPath),
);
break;
case ERC20BridgeSource.MultiBridge:
@ -338,6 +331,14 @@ function createMultiBridgeData(takerToken: string, makerToken: string): string {
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(
curveAddress: string,
fromTokenIdx: number,
@ -404,10 +405,10 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
};
}
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
return {
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 { ZERO_AMOUNT } from './constants';
import { getPathSize, isValidPath } from './fills';
import { getPathAdjustedSize, getPathSize, isValidPath } from './fills';
import { Fill } from './types';
// 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
* (for buys) output, while meeting the input requirement.
@ -16,11 +18,15 @@ export function findOptimalPath(
side: MarketOperation,
paths: Fill[][],
targetInput: BigNumber,
runLimit?: number,
runLimit: number = 2 ** 15,
): Fill[] | undefined {
let optimalPath = paths[0] || [];
for (const path of paths.slice(1)) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit);
// Sort paths in descending order by adjusted output amount.
const sortedPaths = paths
.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;
}
@ -30,7 +36,7 @@ function mixPaths(
pathA: Fill[],
pathB: Fill[],
targetInput: BigNumber,
maxSteps: number = 2 ** 15,
maxSteps: number,
): Fill[] {
let bestPath: Fill[] = [];
let bestPathInput = ZERO_AMOUNT;

View File

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

View File

@ -1,9 +1,20 @@
import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..';
import { getCurveInfo, isCurveSource } from '../source_utils';
import * as _ from 'lodash';
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 { 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,
@ -34,12 +45,9 @@ export const samplerOperations = {
},
};
},
getKyberSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
getKyberSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Kyber,
encodeCall: contract => {
return contract
.sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts)
@ -55,8 +63,9 @@ export const samplerOperations = {
takerToken: string,
makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Kyber,
encodeCall: contract => {
return contract
.sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts, fakeBuyOpts)
@ -67,12 +76,9 @@ export const samplerOperations = {
},
};
},
getUniswapSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
getUniswapSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Uniswap,
encodeCall: contract => {
return contract
.sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts)
@ -83,12 +89,9 @@ export const samplerOperations = {
},
};
},
getUniswapBuyQuotes(
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
getUniswapBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Uniswap,
encodeCall: contract => {
return contract
.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 {
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
encodeCall: contract => {
return contract
.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 {
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
encodeCall: contract => {
return contract
.sampleBuysFromUniswapV2(tokenAddressPath, makerFillAmounts)
@ -128,8 +141,9 @@ export const samplerOperations = {
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation {
return {
source: ERC20BridgeSource.LiquidityProvider,
encodeCall: contract => {
return contract
.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(
registryAddress: string,
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation {
return {
source: ERC20BridgeSource.LiquidityProvider,
encodeCall: contract => {
return contract
.sampleBuysFromLiquidityProviderRegistry(
@ -197,12 +185,34 @@ export const samplerOperations = {
},
};
},
getEth2DaiSellQuotes(
getMultiBridgeSellQuotes(
multiBridgeAddress: string,
makerToken: string,
intermediateToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation {
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 => {
return contract
.sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts)
@ -213,12 +223,9 @@ export const samplerOperations = {
},
};
},
getEth2DaiBuyQuotes(
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
getEth2DaiBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation {
return {
source: ERC20BridgeSource.Eth2Dai,
encodeCall: contract => {
return contract
.sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts)
@ -234,8 +241,14 @@ export const samplerOperations = {
fromTokenIdx: number,
toTokenIdx: number,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation<CurveFillData> {
return {
source: ERC20BridgeSource.Curve,
fillData: {
poolAddress: curveAddress,
fromTokenIdx,
toTokenIdx,
},
encodeCall: contract => {
return contract
.sampleSellsFromCurve(
@ -256,8 +269,14 @@ export const samplerOperations = {
fromTokenIdx: number,
toTokenIdx: number,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
): SourceQuoteOperation<CurveFillData> {
return {
source: ERC20BridgeSource.Curve,
fillData: {
poolAddress: curveAddress,
fromTokenIdx,
toTokenIdx,
},
encodeCall: contract => {
return contract
.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[],
makerToken: string,
takerToken: string,
takerFillAmount: BigNumber,
wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string,
): BatchedOperation<BigNumber> {
): Promise<BatchedOperation<BigNumber>> => {
if (makerToken.toLowerCase() === takerToken.toLowerCase()) {
return samplerOperations.constant(new BigNumber(1));
}
const getSellQuotes = samplerOperations.getSellQuotes(
const getSellQuotes = await samplerOperations.getSellQuotesAsync(
sources,
makerToken,
takerToken,
[takerFillAmount],
wethAddress,
balancerPoolsCache,
liquidityProviderRegistryAddress,
multiBridgeAddress,
);
@ -343,181 +378,221 @@ export const samplerOperations = {
},
};
},
getSellQuotes(
getSellQuotesAsync: async (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string,
): BatchedOperation<DexSample[][]> {
const subOps = sources
.map(source => {
let batchedOperation;
if (source === ERC20BridgeSource.Eth2Dai) {
batchedOperation = samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Uniswap) {
batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.UniswapV2) {
batchedOperation = samplerOperations.getUniswapV2SellQuotes(
[takerToken, makerToken],
takerFillAmounts,
);
} else if (source === ERC20BridgeSource.UniswapV2Eth) {
batchedOperation = samplerOperations.getUniswapV2SellQuotes(
[takerToken, wethAddress, makerToken],
takerFillAmounts,
);
} else if (source === ERC20BridgeSource.Kyber) {
batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (isCurveSource(source)) {
const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken);
if (fromTokenIdx !== -1 && toTokenIdx !== -1) {
batchedOperation = samplerOperations.getCurveSellQuotes(
curveAddress,
fromTokenIdx,
toTokenIdx,
takerFillAmounts,
);
}
} else if (source === ERC20BridgeSource.LiquidityProvider) {
if (liquidityProviderRegistryAddress === undefined) {
throw new Error(
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
);
}
batchedOperation = samplerOperations.getLiquidityProviderSellQuotes(
liquidityProviderRegistryAddress,
makerToken,
takerToken,
takerFillAmounts,
);
} else if (source === ERC20BridgeSource.MultiBridge) {
if (multiBridgeAddress === undefined) {
throw new Error('Cannot sample liquidity from MultiBridge if an address is not provided.');
}
const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken);
batchedOperation = samplerOperations.getMultiBridgeSellQuotes(
multiBridgeAddress,
makerToken,
intermediateToken,
takerToken,
takerFillAmounts,
);
} else {
throw new Error(`Unsupported sell sample source: ${source}`);
}
return { batchedOperation, source };
})
.filter(op => op.batchedOperation) as Array<{
batchedOperation: BatchedOperation<BigNumber[]>;
source: ERC20BridgeSource;
}>;
): Promise<BatchedOperation<DexSample[][]>> => {
const subOps = _.flatten(
await Promise.all(
sources.map(
async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => {
switch (source) {
case ERC20BridgeSource.Eth2Dai:
return samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts);
case ERC20BridgeSource.Uniswap:
return samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
case ERC20BridgeSource.UniswapV2:
const ops = [
samplerOperations.getUniswapV2SellQuotes(
[takerToken, makerToken],
takerFillAmounts,
),
];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
ops.push(
samplerOperations.getUniswapV2SellQuotes(
[takerToken, wethAddress, makerToken],
takerFillAmounts,
),
);
}
return ops;
case ERC20BridgeSource.Kyber:
return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
case ERC20BridgeSource.Curve:
return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress =>
samplerOperations.getCurveSellQuotes(
curveAddress,
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken),
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken),
takerFillAmounts,
),
);
case ERC20BridgeSource.LiquidityProvider:
if (liquidityProviderRegistryAddress === undefined) {
throw new Error(
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
);
}
return samplerOperations.getLiquidityProviderSellQuotes(
liquidityProviderRegistryAddress,
makerToken,
takerToken,
takerFillAmounts,
);
case ERC20BridgeSource.MultiBridge:
if (multiBridgeAddress === undefined) {
throw new Error(
'Cannot sample liquidity from MultiBridge if an address is not provided.',
);
}
const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken);
return samplerOperations.getMultiBridgeSellQuotes(
multiBridgeAddress,
makerToken,
intermediateToken,
takerToken,
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 {
encodeCall: contract => {
const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract));
const subCalls = samplerOps.map(op => op.encodeCall(contract));
return contract.batchCall(subCalls).getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await Promise.all(
subOps.map(async (op, i) =>
op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]),
),
let samples = await Promise.all(
samplerOps.map(async (op, i) => op.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) => ({
source: op.source,
output,
input: takerFillAmounts[j],
fillData: op.fillData,
}));
});
},
};
},
getBuyQuotes(
getBuyQuotesAsync: async (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string,
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): BatchedOperation<DexSample[][]> {
const subOps = sources
.map(source => {
let batchedOperation;
if (source === ERC20BridgeSource.Eth2Dai) {
batchedOperation = samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts);
} else if (source === ERC20BridgeSource.Uniswap) {
batchedOperation = samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts);
} else if (source === ERC20BridgeSource.UniswapV2) {
batchedOperation = samplerOperations.getUniswapV2BuyQuotes(
[takerToken, makerToken],
makerFillAmounts,
);
} else if (source === ERC20BridgeSource.UniswapV2Eth) {
batchedOperation = samplerOperations.getUniswapV2BuyQuotes(
[takerToken, wethAddress, makerToken],
makerFillAmounts,
);
} else if (source === ERC20BridgeSource.Kyber) {
batchedOperation = samplerOperations.getKyberBuyQuotes(
makerToken,
takerToken,
makerFillAmounts,
fakeBuyOpts,
);
} else if (isCurveSource(source)) {
const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken);
if (fromTokenIdx !== -1 && toTokenIdx !== -1) {
batchedOperation = samplerOperations.getCurveBuyQuotes(
curveAddress,
fromTokenIdx,
toTokenIdx,
makerFillAmounts,
);
}
} else if (source === ERC20BridgeSource.LiquidityProvider) {
if (liquidityProviderRegistryAddress === undefined) {
throw new Error(
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
);
}
batchedOperation = samplerOperations.getLiquidityProviderBuyQuotes(
liquidityProviderRegistryAddress,
makerToken,
takerToken,
makerFillAmounts,
fakeBuyOpts,
);
} else {
throw new Error(`Unsupported buy sample source: ${source}`);
}
return { source, batchedOperation };
})
.filter(op => op.batchedOperation) as Array<{
batchedOperation: BatchedOperation<BigNumber[]>;
source: ERC20BridgeSource;
}>;
): Promise<BatchedOperation<DexSample[][]>> => {
const subOps = _.flatten(
await Promise.all(
sources.map(
async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => {
switch (source) {
case ERC20BridgeSource.Eth2Dai:
return samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts);
case ERC20BridgeSource.Uniswap:
return samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts);
case ERC20BridgeSource.UniswapV2:
const ops = [
samplerOperations.getUniswapV2BuyQuotes([takerToken, makerToken], makerFillAmounts),
];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
ops.push(
samplerOperations.getUniswapV2BuyQuotes(
[takerToken, wethAddress, makerToken],
makerFillAmounts,
),
);
}
return ops;
case ERC20BridgeSource.Kyber:
return samplerOperations.getKyberBuyQuotes(
makerToken,
takerToken,
makerFillAmounts,
fakeBuyOpts,
);
case ERC20BridgeSource.Curve:
return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress =>
samplerOperations.getCurveBuyQuotes(
curveAddress,
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken),
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken),
makerFillAmounts,
),
);
case ERC20BridgeSource.LiquidityProvider:
if (liquidityProviderRegistryAddress === undefined) {
throw new Error(
'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.',
);
}
return samplerOperations.getLiquidityProviderBuyQuotes(
liquidityProviderRegistryAddress,
makerToken,
takerToken,
makerFillAmounts,
fakeBuyOpts,
);
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.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 {
encodeCall: contract => {
const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract));
const subCalls = samplerOps.map(op => op.encodeCall(contract));
return contract.batchCall(subCalls).getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await Promise.all(
subOps.map(async (op, i) =>
op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]),
),
let samples = await Promise.all(
samplerOps.map(async (op, i) => op.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) => ({
source: op.source,
output,
input: makerFillAmounts[j],
fillData: op.fillData,
}));
});
},

View File

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

View File

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

View File

@ -12,9 +12,15 @@ import { SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
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 { 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';
const CHAIN_ID = 1;
@ -149,7 +155,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => {
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
@ -158,12 +164,13 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@ -173,6 +180,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
},
],
]);
@ -183,7 +191,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => {
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
@ -192,12 +200,13 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getBuyQuotes(
await DexOrderSampler.ops.getBuyQuotesAsync(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@ -207,6 +216,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000),
fillData: undefined,
},
],
]);
@ -233,12 +243,13 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.MultiBridge],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
randomAddress(),
dexOrderSampler.balancerPoolsCache,
randomAddress(),
multiBridge,
),
@ -249,6 +260,7 @@ describe('DexSampler tests', () => {
source: 'MultiBridge',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
},
],
]);
@ -412,72 +424,92 @@ describe('DexSampler tests', () => {
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
},
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);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getSellQuotes(
await DexOrderSampler.ops.getSellQuotesAsync(
sources,
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s =>
expectedTakerFillAmounts.map(a => ({
source: s,
input: a,
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() includes ETH for Uniswap_V2_ETH', async () => {
it('getSellQuotes() uses samples from Balancer', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.UniswapV2Eth];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.UniswapV2Eth]: getRandomFloat(0, 100),
};
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const sampler = new MockSamplerContract({
sampleSellsFromUniswapV2: (path, fillAmounts) => {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2Eth]).integerValue());
const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
const balancerPoolsCache = new MockBalancerPoolsCache({
getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(takerToken).equal(expectedTakerToken);
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(
DexOrderSampler.ops.getSellQuotes(
sources,
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.Balancer],
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s =>
const expectedQuotes = pools.map(p =>
expectedTakerFillAmounts.map(a => ({
source: s,
source: ERC20BridgeSource.Balancer,
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);
});
it('getBuyQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap];
const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Eth2Dai]: 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());
},
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);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getBuyQuotes(
await DexOrderSampler.ops.getBuyQuotesAsync(
sources,
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s =>
expectedMakerFillAmounts.map(a => ({
source: s,
input: a,
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 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 sampler = new MockSamplerContract({
sampleBuysFromUniswap: (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.Uniswap]).integerValue());
},
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 pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
const balancerPoolsCache = new MockBalancerPoolsCache({
getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(takerToken).equal(expectedTakerToken);
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(
DexOrderSampler.ops.getBuyQuotes(
sources,
await DexOrderSampler.ops.getBuyQuotesAsync(
[ERC20BridgeSource.Balancer],
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
expect(quotes).to.be.length(sources.length);
const expectedQuotes = sources.map(s =>
const expectedQuotes = pools.map(p =>
expectedMakerFillAmounts.map(a => ({
source: s,
source: ERC20BridgeSource.Balancer,
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);
});
});
@ -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

View File

@ -16,16 +16,10 @@ import * as _ from 'lodash';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import {
BUY_SOURCES,
DEFAULT_CURVE_OPTS,
POSITIVE_INF,
SELL_SOURCES,
ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants';
import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants';
import { createFillPaths } from '../src/utils/market_operation_utils/fills';
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
describe('MarketOperationUtils tests', () => {
@ -93,10 +87,7 @@ describe('MarketOperationUtils tests', () => {
case UNISWAP_V2_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.UniswapV2;
case CURVE_BRIDGE_ADDRESS.toLowerCase():
const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter(
k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
);
return curveSource[0] as ERC20BridgeSource;
return ERC20BridgeSource.Curve;
default:
break;
}
@ -132,12 +123,18 @@ describe('MarketOperationUtils tests', () => {
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[] = [];
inputs.forEach((input, i) => {
const rate = rates[i];
samples.push({
source,
fillData: fillData || DEFAULT_FILL_DATA[source],
input: new BigNumber(input),
output: new BigNumber(input)
.minus(i === 0 ? 0 : samples[i - 1].input)
@ -161,10 +158,10 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
_makerToken: string,
_takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
_wethAddress: string,
) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
};
@ -184,10 +181,11 @@ describe('MarketOperationUtils tests', () => {
takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
_balancerPoolsCache?: any,
liquidityProviderAddress?: string,
) => {
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
liquidityPoolParams.sources = sources;
liquidityPoolParams.sources = liquidityPoolParams.sources.concat(sources);
return tradeOperation(rates)(
sources,
makerToken,
@ -203,10 +201,10 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
_makerToken: string,
_takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
_wethAddress: string,
) => {
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 {
return (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
_sources: ERC20BridgeSource[],
_makerToken: string,
_takerToken: string,
_fillAmounts: BigNumber[],
_wethAddress: string,
) => {
return new BigNumber(rate);
};
}
function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation {
return (registryAddress: string, takerToken: string, makerToken: string): string => {
return (_registryAddress: string, _takerToken: string, _makerToken: string): string => {
return NULL_ADDRESS;
};
}
@ -288,17 +286,23 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.UniswapV2]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.UniswapV2Eth]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDai]: _.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.UniswapV2]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.Balancer]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.Curve]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.LiquidityProvider]: _.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 = {
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount);
@ -306,9 +310,9 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount);
},
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRateAsync: createGetMedianSellRate(1),
getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(),
};
@ -346,11 +350,7 @@ describe('MarketOperationUtils tests', () => {
sampleDistributionBase: 1,
bridgeSlippage: 0,
maxFallbackSlippage: 100,
excludedSources: [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2Eth,
...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]),
],
excludedSources: [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer],
allowFallback: false,
shouldBatchBridgeOrders: false,
};
@ -363,9 +363,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0;
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
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, {
@ -378,9 +378,9 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice();
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -396,7 +396,7 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES,
);
replaceSamplerOps({
getSellQuotes: fn,
getSellQuotesAsync: fn,
});
const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils(
@ -419,9 +419,9 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice();
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -477,7 +477,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
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);
}
});
@ -485,11 +485,11 @@ describe('MarketOperationUtils tests', () => {
it('can mix convex sources', async () => {
const rates: RatesBySource = {};
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.Kyber] = [0, 0, 0, 0]; // unused
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -499,7 +499,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
@ -514,18 +514,20 @@ describe('MarketOperationUtils tests', () => {
const nativeFeeRate = 0.06;
const rates: RatesBySource = {
[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.Kyber]: [0.1, 0.1, 0.1, 0.1],
};
const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
[ERC20BridgeSource.Native]: _.constant(
FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
),
};
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -535,7 +537,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native,
];
@ -551,16 +553,18 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
// 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 = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
[ERC20BridgeSource.Uniswap]: _.constant(
FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
),
};
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -571,7 +575,7 @@ describe('MarketOperationUtils tests', () => {
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
});
@ -580,12 +584,12 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = {
[ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use
[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],
};
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -595,7 +599,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native,
];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
@ -604,11 +608,11 @@ describe('MarketOperationUtils tests', () => {
it('fallback orders use different sources', async () => {
const rates: RatesBySource = {};
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.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -620,7 +624,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
];
const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
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 () => {
const rates: RatesBySource = {};
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.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -642,7 +646,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
);
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[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
@ -667,7 +671,7 @@ describe('MarketOperationUtils tests', () => {
] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress);
replaceSamplerOps({
getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT],
getSellQuotes: getSellQuotesFn,
getSellQuotesAsync: getSellQuotesFn,
getLiquidityProviderFromRegistry: getLiquidityProviderFn,
});
@ -706,12 +710,12 @@ describe('MarketOperationUtils tests', () => {
it('batches contiguous bridge sources', async () => {
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.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({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -721,7 +725,7 @@ describe('MarketOperationUtils tests', () => {
numSamples: 4,
excludedSources: [
ERC20BridgeSource.Kyber,
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai),
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve),
],
shouldBatchBridgeOrders: true,
},
@ -729,9 +733,9 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.be.length(3);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.UniswapV2],
[ERC20BridgeSource.Uniswap],
[ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.CurveUsdcDai],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve],
]);
});
});
@ -748,10 +752,10 @@ describe('MarketOperationUtils tests', () => {
bridgeSlippage: 0,
maxFallbackSlippage: 100,
excludedSources: [
...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]),
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2Eth,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer,
],
allowFallback: false,
shouldBatchBridgeOrders: false,
@ -765,9 +769,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, 16);
let actualNumSamples = 0;
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
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, {
@ -780,9 +784,9 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice();
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -798,7 +802,7 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES,
);
replaceSamplerOps({
getBuyQuotes: fn,
getBuyQuotesAsync: fn,
});
const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils(
@ -821,9 +825,9 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sources.slice();
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -879,7 +883,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
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);
}
});
@ -887,10 +891,10 @@ describe('MarketOperationUtils tests', () => {
it('can mix convex sources', async () => {
const rates: RatesBySource = {};
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];
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -900,7 +904,7 @@ describe('MarketOperationUtils tests', () => {
const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
@ -915,18 +919,20 @@ describe('MarketOperationUtils tests', () => {
const nativeFeeRate = 0.06;
const rates: RatesBySource = {
[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.Kyber]: [0.1, 0.1, 0.1, 0.1],
};
const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
[ERC20BridgeSource.Native]: _.constant(
FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
),
};
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -935,7 +941,7 @@ describe('MarketOperationUtils tests', () => {
);
const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
@ -950,17 +956,19 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
// 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],
};
const feeSchedule = {
[ERC20BridgeSource.UniswapV2]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
[ERC20BridgeSource.Uniswap]: _.constant(
FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
),
};
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -971,7 +979,7 @@ describe('MarketOperationUtils tests', () => {
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
});
@ -979,10 +987,10 @@ describe('MarketOperationUtils tests', () => {
it('fallback orders use different sources', async () => {
const rates: RatesBySource = {};
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];
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -994,7 +1002,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.UniswapV2,
ERC20BridgeSource.Uniswap,
];
const secondSources = [ERC20BridgeSource.Eth2Dai];
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 () => {
const rates: RatesBySource = {};
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];
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1015,7 +1023,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
);
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[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
@ -1025,9 +1033,9 @@ describe('MarketOperationUtils tests', () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.5, 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({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1042,7 +1050,7 @@ describe('MarketOperationUtils tests', () => {
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.UniswapV2],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
]);
});
});
@ -1078,7 +1086,7 @@ describe('MarketOperationUtils tests', () => {
};
const orders = [smallOrder, largeOrder];
const feeSchedule = {
[ERC20BridgeSource.Native]: new BigNumber(2e5),
[ERC20BridgeSource.Native]: _.constant(2e5),
};
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 DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_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).
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 {
if (callData === '0x') {
return callData;
}
// tslint:disable-next-line: custom-no-magic-numbers
const selector = hexUtils.slice(callData, 0, 4);
for (const [name, handler] of Object.entries(this._handlers)) {

View File

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

View File

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

View File

@ -34,6 +34,7 @@ export interface ContractAddresses {
maximumGasPrice: string;
dexForwarderBridge: string;
multiBridge: string;
balancerBridge: string;
exchangeProxyGovernor: string;
exchangeProxy: 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",
"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,
dexForwarderBridge: NULL_ADDRESS,
multiBridge: NULL_ADDRESS,
balancerBridge: NULL_ADDRESS,
exchangeProxyGovernor: NULL_ADDRESS,
exchangeProxy: exchangeProxy.address,
exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress,

View File

@ -9,6 +9,10 @@
{
"note": "Add `Set` to `EXTERNAL_TYPE_MAP`.",
"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
Buy: true,
Sell: true,
// HACK: Asset-swapper specifies TFillData as any type that extends FillData
TFillData: true,
IterableIterator: 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",
"changes": [

View File

@ -65,7 +65,7 @@
"no-lodash-isnull": true,
"no-lodash-isundefined": true,
"no-misused-new": true,
"no-non-null-assertion": true,
"no-non-null-assertion": false,
"no-parameter-reassignment": true,
"no-redundant-jsdoc": 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 };

View File

@ -1047,6 +1047,16 @@
lodash "^4.17.13"
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":
version "1.0.3"
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"
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"
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"
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:
version "6.4.0"
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"
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:
version "4.0.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65"
@ -9771,7 +9809,7 @@ isobject@^4.0.0:
version "4.0.0"
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"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies:
@ -16802,6 +16840,11 @@ typescript@3.5.x:
version "3.5.3"
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:
version "1.2.0"
resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195"