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