Merge branch 'development' into feat/add-cream

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

View File

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

View File

@ -0,0 +1,96 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "../interfaces/IERC20Bridge.sol";
import "../interfaces/IShell.sol";
contract ShellBridge is
IERC20Bridge,
IWallet,
DeploymentConstants
{
/// @dev Swaps specified tokens against the Shell contract
/// @param toTokenAddress The token to give to `to`.
/// @param from The maker (this contract).
/// @param to The recipient of the bought tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param bridgeData The abi-encoded "from" token address.
/// @return success The magic bytes if successful.
// solhint-disable no-unused-vars
function bridgeTransferFrom(
address toTokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success)
{
// Decode the bridge data to get the `fromTokenAddress`.
(address fromTokenAddress) = abi.decode(bridgeData, (address));
uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this));
IShell exchange = IShell(_getShellAddress());
// Grant an allowance to the exchange to spend `fromTokenAddress` token.
LibERC20Token.approveIfBelow(fromTokenAddress, address(exchange), fromTokenBalance);
// Try to sell all of this contract's `fromTokenAddress` token balance.
uint256 boughtAmount = exchange.originSwap(
fromTokenAddress,
toTokenAddress,
fromTokenBalance,
amount, // min amount
block.timestamp + 1
);
LibERC20Token.transfer(toTokenAddress, to, boughtAmount);
emit ERC20BridgeTransfer(
fromTokenAddress,
toTokenAddress,
fromTokenBalance,
boughtAmount,
from,
to
);
return BRIDGE_SUCCESS;
}
/// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker
/// and sign for itself in orders. Always succeeds.
/// @return magicValue Magic success bytes, always.
function isValidSignature(
bytes32,
bytes calldata
)
external
view
returns (bytes4 magicValue)
{
return LEGACY_WALLET_MAGIC_VALUE;
}
}

View File

@ -0,0 +1,34 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IShell {
function originSwap(
address from,
address to,
uint256 fromAmount,
uint256 minTargetAmount,
uint256 deadline
)
external
returns (uint256 toAmount);
}

View File

@ -38,7 +38,7 @@
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES"
},
"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."
},
"repository": {

View File

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

View File

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

View File

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

View File

@ -31,6 +31,7 @@ export * from '../test/generated-wrappers/i_gas_token';
export * from '../test/generated-wrappers/i_kyber_network_proxy';
export * from '../test/generated-wrappers/i_m_stable';
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_factory';
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/multi_asset_proxy';
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/sushi_swap_bridge';
export * from '../test/generated-wrappers/test_bancor_bridge';

View File

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

View File

@ -56,6 +56,8 @@ contract DeploymentConstants {
address constant private MUSD_ADDRESS = 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5;
/// @dev Mainnet address of the Mooniswap Registry contract
address constant private MOONISWAP_REGISTRY = 0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303;
/// @dev Mainnet address of the Shell contract
address constant private SHELL_CONTRACT = 0x2E703D658f8dd21709a7B458967aB4081F8D3d05;
// // Ropsten addresses ///////////////////////////////////////////////////////
// /// @dev Mainnet address of the WETH contract.
@ -296,4 +298,14 @@ contract DeploymentConstants {
{
return MOONISWAP_REGISTRY;
}
/// @dev An overridable way to retrieve the Shell contract address.
/// @return registry The Shell contract address.
function _getShellAddress()
internal
view
returns (address)
{
return SHELL_CONTRACT;
}
}

View File

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

View File

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

View File

@ -0,0 +1,63 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
library LibLiquidityProviderRichErrors {
// solhint-disable func-name-mixedcase
function LiquidityProviderIncompleteSellError(
address providerAddress,
address makerToken,
address takerToken,
uint256 sellAmount,
uint256 boughtAmount,
uint256 minBuyAmount
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("LiquidityProviderIncompleteSellError(address,address,address,uint256,uint256,uint256)")),
providerAddress,
makerToken,
takerToken,
sellAmount,
boughtAmount,
minBuyAmount
);
}
function NoLiquidityProviderForMarketError(
address xAsset,
address yAsset
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("NoLiquidityProviderForMarketError(address,address)")),
xAsset,
yAsset
);
}
}

View File

@ -0,0 +1,66 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
/// @dev Feature to swap directly with an on-chain liquidity provider.
interface ILiquidityProviderFeature {
event LiquidityProviderForMarketUpdated(
address indexed xAsset,
address indexed yAsset,
address providerAddress
);
function sellToLiquidityProvider(
address makerToken,
address takerToken,
address payable recipient,
uint256 sellAmount,
uint256 minBuyAmount
)
external
payable
returns (uint256 boughtAmount);
/// @dev Sets address of the liquidity provider for a market given
/// (xAsset, yAsset).
/// @param xAsset First asset managed by the liquidity provider.
/// @param yAsset Second asset managed by the liquidity provider.
/// @param providerAddress Address of the liquidity provider.
function setLiquidityProviderForMarket(
address xAsset,
address yAsset,
address providerAddress
)
external;
/// @dev Returns the address of the liquidity provider for a market given
/// (xAsset, yAsset), or reverts if pool does not exist.
/// @param xAsset First asset managed by the liquidity provider.
/// @param yAsset Second asset managed by the liquidity provider.
/// @return providerAddress Address of the liquidity provider.
function getLiquidityProviderForMarket(
address xAsset,
address yAsset
)
external
view
returns (address providerAddress);
}

View File

@ -0,0 +1,200 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../errors/LibLiquidityProviderRichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../migrations/LibMigrate.sol";
import "../storage/LibLiquidityProviderStorage.sol";
import "../vendor/v3/IERC20Bridge.sol";
import "./IFeature.sol";
import "./ILiquidityProviderFeature.sol";
import "./ITokenSpenderFeature.sol";
contract LiquidityProviderFeature is
IFeature,
ILiquidityProviderFeature,
FixinCommon
{
using LibERC20TokenV06 for IERC20TokenV06;
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "LiquidityProviderFeature";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev ETH pseudo-token address.
address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @dev The WETH contract address.
IEtherTokenV06 public immutable weth;
/// @dev Store the WETH address in an immutable.
/// @param weth_ The weth token.
constructor(IEtherTokenV06 weth_)
public
FixinCommon()
{
weth = weth_;
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate()
external
returns (bytes4 success)
{
_registerFeatureFunction(this.sellToLiquidityProvider.selector);
_registerFeatureFunction(this.setLiquidityProviderForMarket.selector);
_registerFeatureFunction(this.getLiquidityProviderForMarket.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
function sellToLiquidityProvider(
address makerToken,
address takerToken,
address payable recipient,
uint256 sellAmount,
uint256 minBuyAmount
)
external
override
payable
returns (uint256 boughtAmount)
{
address providerAddress = getLiquidityProviderForMarket(makerToken, takerToken);
if (recipient == address(0)) {
recipient = msg.sender;
}
if (takerToken == ETH_TOKEN_ADDRESS) {
// Wrap ETH.
weth.deposit{value: sellAmount}();
weth.transfer(providerAddress, sellAmount);
} else {
ITokenSpenderFeature(address(this))._spendERC20Tokens(
IERC20TokenV06(takerToken),
msg.sender,
providerAddress,
sellAmount
);
}
if (makerToken == ETH_TOKEN_ADDRESS) {
uint256 balanceBefore = weth.balanceOf(address(this));
IERC20Bridge(providerAddress).bridgeTransferFrom(
address(weth),
address(0),
address(this),
minBuyAmount,
""
);
boughtAmount = weth.balanceOf(address(this)).safeSub(balanceBefore);
// Unwrap wETH and send ETH to recipient.
weth.withdraw(boughtAmount);
recipient.transfer(boughtAmount);
} else {
uint256 balanceBefore = IERC20TokenV06(makerToken).balanceOf(recipient);
IERC20Bridge(providerAddress).bridgeTransferFrom(
makerToken,
address(0),
recipient,
minBuyAmount,
""
);
boughtAmount = IERC20TokenV06(makerToken).balanceOf(recipient).safeSub(balanceBefore);
}
if (boughtAmount < minBuyAmount) {
LibLiquidityProviderRichErrors.LiquidityProviderIncompleteSellError(
providerAddress,
makerToken,
takerToken,
sellAmount,
boughtAmount,
minBuyAmount
).rrevert();
}
}
/// @dev Sets address of the liquidity provider for a market given
/// (xAsset, yAsset).
/// @param xAsset First asset managed by the liquidity provider.
/// @param yAsset Second asset managed by the liquidity provider.
/// @param providerAddress Address of the liquidity provider.
function setLiquidityProviderForMarket(
address xAsset,
address yAsset,
address providerAddress
)
external
override
onlyOwner
{
LibLiquidityProviderStorage.getStorage()
.addressBook[xAsset][yAsset] = providerAddress;
LibLiquidityProviderStorage.getStorage()
.addressBook[yAsset][xAsset] = providerAddress;
emit LiquidityProviderForMarketUpdated(
xAsset,
yAsset,
providerAddress
);
}
/// @dev Returns the address of the liquidity provider for a market given
/// (xAsset, yAsset), or reverts if pool does not exist.
/// @param xAsset First asset managed by the liquidity provider.
/// @param yAsset Second asset managed by the liquidity provider.
/// @return providerAddress Address of the liquidity provider.
function getLiquidityProviderForMarket(
address xAsset,
address yAsset
)
public
view
override
returns (address providerAddress)
{
if (xAsset == ETH_TOKEN_ADDRESS) {
providerAddress = LibLiquidityProviderStorage.getStorage()
.addressBook[address(weth)][yAsset];
} else if (yAsset == ETH_TOKEN_ADDRESS) {
providerAddress = LibLiquidityProviderStorage.getStorage()
.addressBook[xAsset][address(weth)];
} else {
providerAddress = LibLiquidityProviderStorage.getStorage()
.addressBook[xAsset][yAsset];
}
if (providerAddress == address(0)) {
LibLiquidityProviderRichErrors.NoLiquidityProviderForMarketError(
xAsset,
yAsset
).rrevert();
}
}
}

View File

@ -184,7 +184,7 @@ contract MetaTransactionsFeature is
/// @dev Execute a meta-transaction via `sender`. Privileged variant.
/// 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 signature The signature by `mtx.signer`.
/// @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.
/// 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)
private
returns (bytes memory returnResult)

View File

@ -0,0 +1,45 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "./LibStorage.sol";
/// @dev Storage helpers for `LiquidityProviderFeature`.
library LibLiquidityProviderStorage {
/// @dev Storage bucket for this feature.
struct Storage {
// Mapping of taker token -> maker token -> liquidity provider address
// Note that addressBook[x][y] == addressBook[y][x] will always hold.
mapping (address => mapping (address => address)) addressBook;
}
/// @dev Get the storage bucket for this contract.
function getStorage() internal pure returns (Storage storage stor) {
uint256 storageSlot = LibStorage.getStorageSlot(
LibStorage.StorageId.LiquidityProvider
);
// Dip into assembly to change the slot pointed to by the local
// variable `stor`.
// See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries
assembly { stor_slot := storageSlot }
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,84 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
interface IShell {
function originSwap(
address from,
address to,
uint256 fromAmount,
uint256 minTargetAmount,
uint256 deadline
)
external
returns (uint256 toAmount);
}
contract MixinShell is
MixinAdapterAddresses
{
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the `Shell` contract.
IShell private immutable SHELL;
constructor(AdapterAddresses memory addresses)
public
{
SHELL = IShell(addresses.shell);
}
function _tradeShell(
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
)
internal
returns (uint256 boughtAmount)
{
(address fromTokenAddress) = abi.decode(bridgeData, (address));
// Grant the Shell contract an allowance to sell the first token.
IERC20TokenV06(fromTokenAddress).approveIfBelow(
address(SHELL),
sellAmount
);
uint256 buyAmount = SHELL.originSwap(
fromTokenAddress,
address(buyToken),
// Sell all tokens we hold.
sellAmount,
// Minimum buy amount.
1,
// deadline
block.timestamp + 1
);
return buyAmount;
}
}

View File

@ -0,0 +1,69 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../src/vendor/v3/IERC20Bridge.sol";
contract TestBridge is
IERC20Bridge
{
IERC20TokenV06 public immutable xAsset;
IERC20TokenV06 public immutable yAsset;
constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_)
public
{
xAsset = xAsset_;
yAsset = yAsset_;
}
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`.
/// @param tokenAddress The address of the ERC20 token to transfer.
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
/// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @return success The magic bytes `0xdc1600f3` if successful.
function bridgeTransferFrom(
address tokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
override
returns (bytes4 success)
{
IERC20TokenV06 takerToken = tokenAddress == address(xAsset) ? yAsset : xAsset;
uint256 takerTokenBalance = takerToken.balanceOf(address(this));
emit ERC20BridgeTransfer(
address(takerToken),
tokenAddress,
takerTokenBalance,
amount,
from,
to
);
return 0xdecaf000;
}
}

View File

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

View File

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

View File

@ -16,6 +16,7 @@ export * from '../generated-wrappers/i_token_spender_feature';
export * from '../generated-wrappers/i_transform_erc20_feature';
export * from '../generated-wrappers/i_zero_ex';
export * from '../generated-wrappers/initial_migration';
export * from '../generated-wrappers/liquidity_provider_feature';
export * from '../generated-wrappers/log_metadata_transformer';
export * from '../generated-wrappers/meta_transactions_feature';
export * from '../generated-wrappers/ownable_feature';

View File

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

View File

@ -0,0 +1,250 @@
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
import {
IOwnableFeatureContract,
IZeroExContract,
LiquidityProviderFeatureContract,
TokenSpenderFeatureContract,
} from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
import { IERC20BridgeEvents, TestBridgeContract, TestWethContract } from '../wrappers';
blockchainTests('LiquidityProvider feature', env => {
let zeroEx: IZeroExContract;
let feature: LiquidityProviderFeatureContract;
let token: DummyERC20TokenContract;
let weth: TestWethContract;
let owner: string;
let taker: string;
before(async () => {
[owner, taker] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync(
artifacts.TestTokenSpender,
env.provider,
env.txDefaults,
artifacts,
)).address,
});
const tokenSpender = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis);
const allowanceTarget = await tokenSpender.getAllowanceTarget().callAsync();
token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
erc20Artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
await token.setBalance(taker, constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync();
weth = await TestWethContract.deployFrom0xArtifactAsync(
artifacts.TestWeth,
env.provider,
env.txDefaults,
artifacts,
);
await token
.approve(allowanceTarget, constants.INITIAL_ERC20_ALLOWANCE)
.awaitTransactionSuccessAsync({ from: taker });
feature = new LiquidityProviderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis);
const featureImpl = await LiquidityProviderFeatureContract.deployFrom0xArtifactAsync(
artifacts.LiquidityProviderFeature,
env.provider,
env.txDefaults,
artifacts,
weth.address,
);
await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis)
.migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner)
.awaitTransactionSuccessAsync();
});
describe('Registry', () => {
it('`getLiquidityProviderForMarket` reverts if address is not set', async () => {
const [xAsset, yAsset] = [randomAddress(), randomAddress()];
let tx = feature.getLiquidityProviderForMarket(xAsset, yAsset).awaitTransactionSuccessAsync();
expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset),
);
tx = feature.getLiquidityProviderForMarket(yAsset, xAsset).awaitTransactionSuccessAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(yAsset, xAsset),
);
});
it('can set/get a liquidity provider address for a given market', async () => {
const expectedAddress = randomAddress();
await feature
.setLiquidityProviderForMarket(token.address, weth.address, expectedAddress)
.awaitTransactionSuccessAsync();
let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
});
it('can update a liquidity provider address for a given market', async () => {
const expectedAddress = randomAddress();
await feature
.setLiquidityProviderForMarket(token.address, weth.address, expectedAddress)
.awaitTransactionSuccessAsync();
let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync();
expect(actualAddress).to.equal(expectedAddress);
});
it('can effectively remove a liquidity provider for a market by setting the address to 0', async () => {
await feature
.setLiquidityProviderForMarket(token.address, weth.address, constants.NULL_ADDRESS)
.awaitTransactionSuccessAsync();
const tx = feature
.getLiquidityProviderForMarket(token.address, weth.address)
.awaitTransactionSuccessAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(token.address, weth.address),
);
});
it('reverts if non-owner attempts to set an address', async () => {
const tx = feature
.setLiquidityProviderForMarket(randomAddress(), randomAddress(), randomAddress())
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(taker, owner));
});
});
blockchainTests.resets('Swap', () => {
let liquidityProvider: TestBridgeContract;
const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
before(async () => {
liquidityProvider = await TestBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestBridge,
env.provider,
env.txDefaults,
artifacts,
token.address,
weth.address,
);
await feature
.setLiquidityProviderForMarket(token.address, weth.address, liquidityProvider.address)
.awaitTransactionSuccessAsync();
});
it('Cannot execute a swap for a market without a liquidity provider set', async () => {
const [xAsset, yAsset] = [randomAddress(), randomAddress()];
const tx = feature
.sellToLiquidityProvider(
xAsset,
yAsset,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset),
);
});
it('Successfully executes an ERC20-ERC20 swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
weth.address,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: token.address,
outputToken: weth.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: taker,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
it('Reverts if cannot fulfill the minimum buy amount', async () => {
const minBuyAmount = new BigNumber(1);
const tx = feature
.sellToLiquidityProvider(
weth.address,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
minBuyAmount,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.LiquidityProvider.LiquidityProviderIncompleteSellError(
liquidityProvider.address,
weth.address,
token.address,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
minBuyAmount,
),
);
});
it('Successfully executes an ETH-ERC20 swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
token.address,
ETH_TOKEN_ADDRESS,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker, value: constants.ONE_ETHER });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: weth.address,
outputToken: token.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: taker,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
it('Successfully executes an ERC20-ETH swap', async () => {
const tx = await feature
.sellToLiquidityProvider(
ETH_TOKEN_ADDRESS,
token.address,
constants.NULL_ADDRESS,
constants.ONE_ETHER,
constants.ZERO_AMOUNT,
)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(
tx.logs,
[
{
inputToken: token.address,
outputToken: weth.address,
inputTokenAmount: constants.ONE_ETHER,
outputTokenAmount: constants.ZERO_AMOUNT,
from: constants.NULL_ADDRESS,
to: zeroEx.address,
},
],
IERC20BridgeEvents.ERC20BridgeTransfer,
);
});
});
});

View File

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

View File

@ -22,6 +22,7 @@ export * from '../test/generated-wrappers/i_exchange';
export * from '../test/generated-wrappers/i_feature';
export * from '../test/generated-wrappers/i_flash_wallet';
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_ownable_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_common_rich_errors';
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_storage';
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_storage';
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/meta_transactions_feature';
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_mooniswap';
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_v2';
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/signature_validator_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_delegate_caller';
export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge';

View File

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

View File

@ -133,6 +133,22 @@
{
"note": "Respect max slippage in EP consumer",
"pr": 2712
},
{
"note": "Introduced Path class, exchangeProxyOverhead parameter",
"pr": 2691
},
{
"note": "Added `Shell`",
"pr": 2722
},
{
"note": "Fix exchange proxy overhead gas being scaled by gas price",
"pr": 2723
},
{
"note": "Remove 0x-API swap/v0-specifc code from asset-swapper",
"pr": 2725
}
]
},

View File

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

View File

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

View File

@ -0,0 +1,110 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./interfaces/IShell.sol";
contract ShellSampler is
DeploymentConstants
{
/// @dev Default gas limit for Shell calls.
uint256 constant private DEFAULT_CALL_GAS = 300e3; // 300k
/// @dev Sample sell quotes from the Shell contract
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromShell(
address takerToken,
address makerToken,
uint256[] memory takerTokenAmounts
)
public
view
returns (uint256[] memory makerTokenAmounts)
{
// Initialize array of maker token amounts.
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)(
abi.encodeWithSelector(
IShell(0).viewOriginSwap.selector,
takerToken,
makerToken,
takerTokenAmounts[i]
));
uint256 buyAmount = 0;
if (didSucceed) {
buyAmount = abi.decode(resultData, (uint256));
}
// Exit early if the amount is too high for the source to serve
if (buyAmount == 0) {
break;
}
makerTokenAmounts[i] = buyAmount;
}
}
/// @dev Sample buy quotes from Shell contract
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param makerTokenAmounts Maker token buy amount for each sample.
/// @return takerTokenAmounts Taker amounts sold at each maker token
/// amount.
function sampleBuysFromShell(
address takerToken,
address makerToken,
uint256[] memory makerTokenAmounts
)
public
view
returns (uint256[] memory takerTokenAmounts)
{
// Initialize array of maker token amounts.
uint256 numSamples = makerTokenAmounts.length;
takerTokenAmounts = new uint256[](numSamples);
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)(
abi.encodeWithSelector(
IShell(0).viewTargetSwap.selector,
takerToken,
makerToken,
makerTokenAmounts[i]
));
uint256 sellAmount = 0;
if (didSucceed) {
sellAmount = abi.decode(resultData, (uint256));
}
// Exit early if the amount is too high for the source to serve
if (sellAmount == 0) {
break;
}
takerTokenAmounts[i] = sellAmount;
}
}
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IShell {
function viewOriginSwap (
address from,
address to,
uint256 fromAmount
)
external
view
returns (uint256 toAmount);
function viewTargetSwap (
address from,
address to,
uint256 toAmount
)
external
view
returns (uint256 fromAmount);
}

View File

@ -17,7 +17,7 @@
"compile": "sol-compiler",
"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",
"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",
"test": "yarn run_mocha",
"rebuild_and_test": "run-s clean build test",
@ -38,7 +38,7 @@
"config": {
"publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
"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": {
"assets": []
}

View File

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

View File

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

View File

@ -1,115 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ExchangeContract } from '@0x/contract-wrappers';
import { providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalldataInfo,
MarketOperation,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;
private readonly _exchangeContract: ExchangeContract;
constructor(
supportedProvider: SupportedProvider,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._exchangeContract = new ExchangeContract(contractAddresses.exchange, supportedProvider);
}
public async getCalldataOrThrowAsync(
quote: SwapQuote,
_opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const { orders } = quote;
const signatures = _.map(orders, o => o.signature);
let calldataHexString;
if (quote.type === MarketOperation.Buy) {
calldataHexString = this._exchangeContract
.marketBuyOrdersFillOrKill(orders, quote.makerAssetFillAmount, signatures)
.getABIEncodedTransactionData();
} else {
calldataHexString = this._exchangeContract
.marketSellOrdersFillOrKill(orders, quote.takerAssetFillAmount, signatures)
.getABIEncodedTransactionData();
}
return {
calldataHexString,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
toAddress: this._exchangeContract.address,
allowanceTarget: this.contractAddresses.erc20Proxy,
};
}
public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
assert.isValidSwapQuote('quote', quote);
const { takerAddress, gasLimit, ethAmount } = opts;
if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress);
}
if (gasLimit !== undefined) {
assert.isNumber('gasLimit', gasLimit);
}
if (ethAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount);
}
const { orders, gasPrice } = quote;
const signatures = orders.map(o => o.signature);
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
const value = ethAmount || quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
let txHash: string;
if (quote.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quote;
txHash = await this._exchangeContract
.marketBuyOrdersFillOrKill(orders, makerAssetFillAmount, signatures)
.sendTransactionAsync({
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value,
});
} else {
const { takerAssetFillAmount } = quote;
txHash = await this._exchangeContract
.marketSellOrdersFillOrKill(orders, takerAssetFillAmount, signatures)
.sendTransactionAsync({
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value,
});
}
// TODO(dorothy-zbornak): Handle signature request denied
// (see contract-wrappers/decorators)
// and ExchangeRevertErrors.IncompleteFillError.
return txHash;
}
}

View File

@ -1,198 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ForwarderContract } from '@0x/contract-wrappers';
import { assetDataUtils } from '@0x/order-utils';
import { providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalldataInfo,
MarketOperation,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
const { NULL_ADDRESS } = constants;
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;
public buyQuoteSellAmountScalingFactor = 1.0001; // 100% + 1 bps
private readonly _forwarder: ForwarderContract;
constructor(
supportedProvider: SupportedProvider,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
}
/**
* Given a SwapQuote, returns 'CalldataInfo' for a forwarder extension call. See type definition of CalldataInfo for more information.
* @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting CalldataInfo. See type definition for more information.
*/
public async getCalldataOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { extensionContractOpts } = { ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, ...opts };
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
const { feeRecipient, feePercentage } = extensionContractOpts;
const { orders, worstCaseQuoteInfo } = quote;
const normalizedFeeRecipientAddress = feeRecipient.toLowerCase();
const signatures = _.map(orders, o => o.signature);
const ethAmountWithFees = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(
{
...worstCaseQuoteInfo,
// HACK(dorothy-zbornak): The forwarder contract has a rounding bug
// that causes buys of low-decimal tokens to not complete.
// Scaling the max sell amount by 1bps seems to be sufficient to
// overcome this.
...(quote.type === MarketOperation.Buy
? {
// tslint:disable-next-line: custom-no-magic-numbers
totalTakerAssetAmount: worstCaseQuoteInfo.totalTakerAssetAmount
.times(this.buyQuoteSellAmountScalingFactor)
.integerValue(),
}
: {}),
},
feePercentage,
);
const feeAmount = affiliateFeeUtils.getFeeAmount(worstCaseQuoteInfo, feePercentage);
let calldataHexString;
if (quote.type === MarketOperation.Buy) {
calldataHexString = this._forwarder
.marketBuyOrdersWithEth(
orders,
quote.makerAssetFillAmount,
signatures,
[feeAmount],
[normalizedFeeRecipientAddress],
)
.getABIEncodedTransactionData();
} else {
calldataHexString = this._forwarder
.marketSellAmountWithEth(
orders,
quote.takerAssetFillAmount,
signatures,
[feeAmount],
[normalizedFeeRecipientAddress],
)
.getABIEncodedTransactionData();
}
return {
calldataHexString,
toAddress: this._forwarder.address,
ethAmount: ethAmountWithFees,
allowanceTarget: NULL_ADDRESS,
};
}
/**
* Given a SwapQuote and desired rate (in Eth), attempt to execute the swap.
* @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting CalldataInfo. See type definition for more information.
*/
public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { ethAmount: providedEthAmount, takerAddress, gasLimit, extensionContractOpts } = {
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
...opts,
};
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
const { feeRecipient, feePercentage } = extensionContractOpts;
if (providedEthAmount !== undefined) {
assert.isBigNumber('ethAmount', providedEthAmount);
}
if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress);
}
if (gasLimit !== undefined) {
assert.isNumber('gasLimit', gasLimit);
}
const { orders, gasPrice } = quote; // tslint:disable-line:no-unused-variable
const signatures = orders.map(o => o.signature);
// get taker address
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
// if no ethAmount is provided, default to the worst totalTakerAssetAmount
const ethAmountWithFees =
providedEthAmount ||
affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(quote.worstCaseQuoteInfo, feePercentage);
const feeAmount = affiliateFeeUtils.getFeeAmount(
{
...quote.worstCaseQuoteInfo,
// HACK(dorothy-zbornak): The forwarder contract has a rounding bug
// that causes buys of low-decimal tokens to not complete.
// Scaling the max sell amount by 1bps seems to be sufficient to
// overcome this.
...(quote.type === MarketOperation.Buy
? {
// tslint:disable-next-line: custom-no-magic-numbers
totalTakerAssetAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount
.times(this.buyQuoteSellAmountScalingFactor)
.integerValue(),
}
: {}),
},
feePercentage,
);
let txHash: string;
if (quote.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quote;
txHash = await this._forwarder
.marketBuyOrdersWithEth(orders, makerAssetFillAmount, signatures, [feeAmount], [feeRecipient])
.sendTransactionAsync({
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value: ethAmountWithFees,
});
} else {
txHash = await this._forwarder
.marketSellAmountWithEth(orders, quote.takerAssetFillAmount, signatures, [feeAmount], [feeRecipient])
.sendTransactionAsync({
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value: ethAmountWithFees,
});
}
// TODO(dorothy-zbornak): Handle signature request denied
// (see contract-wrappers/decorators)
// and ForwarderRevertErrors.CompleteBuyFailed.
return txHash;
}
private _getEtherTokenAssetDataOrThrow(): string {
return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
}
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils';
import { SourceFilters } from './source_filters';
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.
@ -22,6 +22,7 @@ export const SELL_SOURCE_FILTER = new SourceFilters([
ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.Shell,
ERC20BridgeSource.MultiHop,
]);
@ -40,6 +41,7 @@ export const BUY_SOURCE_FILTER = new SourceFilters(
// ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes
ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Shell,
ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop,
@ -58,8 +60,8 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
sampleDistributionBase: 1.05,
feeSchedule: {},
gasSchedule: {},
exchangeProxyOverhead: () => ZERO_AMOUNT,
allowFallback: true,
shouldBatchBridgeOrders: true,
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 SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign(
{},
...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })),
);
/**
* Mainnet Curve configuration
*/

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,276 @@
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types';
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders';
import { getCompleteRate, getRate } from './rate_utils';
import {
CollapsedFill,
ERC20BridgeSource,
ExchangeProxyOverhead,
Fill,
NativeCollapsedFill,
OptimizedMarketOrder,
} from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
export interface PathSize {
input: BigNumber;
output: BigNumber;
}
export interface PathPenaltyOpts {
ethToOutputRate: BigNumber;
ethToInputRate: BigNumber;
exchangeProxyOverhead: ExchangeProxyOverhead;
}
export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = {
ethToOutputRate: ZERO_AMOUNT,
ethToInputRate: ZERO_AMOUNT,
exchangeProxyOverhead: () => ZERO_AMOUNT,
};
export class Path {
public collapsedFills?: ReadonlyArray<CollapsedFill>;
public orders?: OptimizedMarketOrder[];
public sourceFlags: number = 0;
protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
protected _adjustedSize: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
public static create(
side: MarketOperation,
fills: ReadonlyArray<Fill>,
targetInput: BigNumber = POSITIVE_INF,
pathPenaltyOpts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Path {
const path = new Path(side, fills, targetInput, pathPenaltyOpts);
fills.forEach(fill => {
path.sourceFlags |= fill.flags;
path._addFillSize(fill);
});
return path;
}
public static clone(base: Path): Path {
const clonedPath = new Path(base.side, base.fills.slice(), base.targetInput, base.pathPenaltyOpts);
clonedPath.sourceFlags = base.sourceFlags;
clonedPath._size = { ...base._size };
clonedPath._adjustedSize = { ...base._adjustedSize };
clonedPath.collapsedFills = base.collapsedFills === undefined ? undefined : base.collapsedFills.slice();
clonedPath.orders = base.orders === undefined ? undefined : base.orders.slice();
return clonedPath;
}
protected constructor(
protected readonly side: MarketOperation,
public fills: ReadonlyArray<Fill>,
protected readonly targetInput: BigNumber,
public readonly pathPenaltyOpts: PathPenaltyOpts,
) {}
public append(fill: Fill): this {
(this.fills as Fill[]).push(fill);
this.sourceFlags |= fill.flags;
this._addFillSize(fill);
return this;
}
public addFallback(fallback: Path): this {
// If the last fill is Native and penultimate is not, then the intention was to partial fill
// In this case we drop it entirely as we can't handle a failure at the end and we don't
// want to fully fill when it gets prepended to the front below
const [last, penultimateIfExists] = this.fills.slice().reverse();
const lastNativeFillIfExists =
last.source === ERC20BridgeSource.Native &&
penultimateIfExists &&
penultimateIfExists.source !== ERC20BridgeSource.Native
? last
: undefined;
// By prepending native paths to the front they cannot split on-chain sources and incur
// an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber]
// In the previous step we dropped any hanging Native partial fills, as to not fully fill
const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native);
this.fills = [...nativeFills.filter(f => f !== lastNativeFillIfExists), ...fallback.fills];
// Recompute the source flags
this.sourceFlags = this.fills.reduce((flags, fill) => flags | fill.flags, 0);
return this;
}
public collapse(opts: CreateOrderFromPathOpts): CollapsedPath {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills;
this.orders = [];
for (let i = 0; i < collapsedFills.length; ) {
if (collapsedFills[i].source === ERC20BridgeSource.Native) {
this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill));
++i;
continue;
}
// If there are contiguous bridge orders, we can batch them together.
const contiguousBridgeFills = [collapsedFills[i]];
for (let j = i + 1; j < collapsedFills.length; ++j) {
if (collapsedFills[j].source === ERC20BridgeSource.Native) {
break;
}
contiguousBridgeFills.push(collapsedFills[j]);
}
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
i += 1;
}
return this as CollapsedPath;
}
public size(): PathSize {
return this._size;
}
public adjustedSize(): PathSize {
const { input, output } = this._adjustedSize;
const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts;
const gasOverhead = exchangeProxyOverhead(this.sourceFlags);
const pathPenalty = !ethToOutputRate.isZero()
? ethToOutputRate.times(gasOverhead)
: ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input));
return {
input,
output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty),
};
}
public adjustedCompleteRate(): BigNumber {
const { input, output } = this.adjustedSize();
return getCompleteRate(this.side, input, output, this.targetInput);
}
public adjustedRate(): BigNumber {
const { input, output } = this.adjustedSize();
return getRate(this.side, input, output);
}
public adjustedSlippage(maxRate: BigNumber): number {
if (maxRate.eq(0)) {
return 0;
}
const totalRate = this.adjustedRate();
const rateChange = maxRate.minus(totalRate);
return rateChange.div(maxRate).toNumber();
}
public isBetterThan(other: Path): boolean {
if (!this.targetInput.isEqualTo(other.targetInput)) {
throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`);
}
const { targetInput } = this;
const { input } = this._size;
const { input: otherInput } = other._size;
if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) {
return input.isGreaterThan(otherInput);
} else {
return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
}
// if (otherInput.isLessThan(targetInput)) {
// return input.isGreaterThan(otherInput);
// } else if (input.isGreaterThanOrEqualTo(targetInput)) {
// return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
// }
// return false;
}
public isComplete(): boolean {
const { input } = this._size;
return input.gte(this.targetInput);
}
public isValid(skipDuplicateCheck: boolean = false): boolean {
for (let i = 0; i < this.fills.length; ++i) {
// Fill must immediately follow its parent.
if (this.fills[i].parent) {
if (i === 0 || this.fills[i - 1] !== this.fills[i].parent) {
return false;
}
}
if (!skipDuplicateCheck) {
// Fill must not be duplicated.
for (let j = 0; j < i; ++j) {
if (this.fills[i] === this.fills[j]) {
return false;
}
}
}
}
return doSourcesConflict(this.sourceFlags);
}
public isValidNextFill(fill: Fill): boolean {
if (this.fills.length === 0) {
return !fill.parent;
}
if (this.fills[this.fills.length - 1] === fill.parent) {
return true;
}
if (fill.parent) {
return false;
}
return doSourcesConflict(this.sourceFlags | fill.flags);
}
private _collapseFills(): ReadonlyArray<CollapsedFill> {
this.collapsedFills = [];
for (const fill of this.fills) {
const source = fill.source;
if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = this.collapsedFills[this.collapsedFills.length - 1];
// If the last fill is from the same source, merge them.
if (prevFill.sourcePathId === fill.sourcePathId) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
prevFill.fillData = fill.fillData;
prevFill.subFills.push(fill);
continue;
}
}
(this.collapsedFills as CollapsedFill[]).push({
sourcePathId: fill.sourcePathId,
source: fill.source,
fillData: fill.fillData,
input: fill.input,
output: fill.output,
subFills: [fill],
});
}
return this.collapsedFills;
}
private _addFillSize(fill: Fill): void {
if (this._size.input.plus(fill.input).isGreaterThan(this.targetInput)) {
const remainingInput = this.targetInput.minus(this._size.input);
const scaledFillOutput = fill.output.times(remainingInput.div(fill.input));
this._size.input = this.targetInput;
this._size.output = this._size.output.plus(scaledFillOutput);
// Penalty does not get interpolated.
const penalty = fill.adjustedOutput.minus(fill.output);
this._adjustedSize.input = this.targetInput;
this._adjustedSize.output = this._adjustedSize.output.plus(scaledFillOutput).plus(penalty);
} else {
this._size.input = this._size.input.plus(fill.input);
this._size.output = this._size.output.plus(fill.output);
this._adjustedSize.input = this._adjustedSize.input.plus(fill.input);
this._adjustedSize.output = this._adjustedSize.output.plus(fill.adjustedOutput);
}
}
}
export interface CollapsedPath extends Path {
readonly collapsedFills: ReadonlyArray<CollapsedFill>;
readonly orders: OptimizedMarketOrder[];
}
const MULTIBRIDGE_SOURCES = SOURCE_FLAGS.LiquidityProvider | SOURCE_FLAGS.Uniswap;
export function doSourcesConflict(flags: number): boolean {
const multiBridgeConflict = flags & SOURCE_FLAGS.MultiBridge && flags & MULTIBRIDGE_SOURCES;
return !multiBridgeConflict;
}

View File

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

View File

@ -0,0 +1,62 @@
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types';
import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types';
/**
* Returns the fee-adjusted rate of a two-hop quote. Returns zero if the
* quote falls short of the target input.
*/
export function getTwoHopAdjustedRate(
side: MarketOperation,
twoHopQuote: DexSample<MultiHopFillData>,
targetInput: BigNumber,
ethToOutputRate: BigNumber,
fees: FeeSchedule = {},
exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT,
): BigNumber {
const { output, input, fillData } = twoHopQuote;
if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT;
}
const penalty = ethToOutputRate.times(
exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
);
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
}
/**
* Computes the "complete" rate given the input/output of a path.
* This value penalizes the path if it falls short of the target input.
*/
export function getCompleteRate(
side: MarketOperation,
input: BigNumber,
output: BigNumber,
targetInput: BigNumber,
): BigNumber {
if (input.eq(0) || output.eq(0) || targetInput.eq(0)) {
return ZERO_AMOUNT;
}
// Penalize paths that fall short of the entire input amount by a factor of
// input / targetInput => (i / t)
if (side === MarketOperation.Sell) {
// (o / i) * (i / t) => (o / t)
return output.div(targetInput);
}
// (i / o) * (i / t)
return input.div(output).times(input.div(targetInput));
}
/**
* Computes the rate given the input/output of a path.
*/
export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
if (input.eq(0) || output.eq(0)) {
return ZERO_AMOUNT;
}
return side === MarketOperation.Sell ? output.div(input) : input.div(output);
}

View File

@ -734,6 +734,32 @@ export class SamplerOperations {
});
}
public getShellSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): SourceQuoteOperation {
return new SamplerContractOperation({
source: ERC20BridgeSource.Shell,
contract: this._samplerContract,
function: this._samplerContract.sampleSellsFromShell,
params: [takerToken, makerToken, takerFillAmounts],
});
}
public getShellBuyQuotes(
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): SourceQuoteOperation {
return new SamplerContractOperation({
source: ERC20BridgeSource.Shell,
contract: this._samplerContract,
function: this._samplerContract.sampleBuysFromShell,
params: [takerToken, makerToken, makerFillAmounts],
});
}
public getMedianSellRate(
sources: ERC20BridgeSource[],
makerToken: string,
@ -971,6 +997,8 @@ export class SamplerOperations {
.map(poolAddress =>
this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts),
);
case ERC20BridgeSource.Shell:
return this.getShellSellQuotes(makerToken, takerToken, takerFillAmounts);
default:
throw new Error(`Unsupported sell sample source: ${source}`);
}
@ -1058,6 +1086,8 @@ export class SamplerOperations {
.map(poolAddress =>
this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts),
);
case ERC20BridgeSource.Shell:
return this.getShellBuyQuotes(makerToken, takerToken, makerFillAmounts);
default:
throw new Error(`Unsupported buy sample source: ${source}`);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import {
} from '../types';
import { MarketOperationUtils } from './market_operation_utils';
import { SOURCE_FLAGS } from './market_operation_utils/constants';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import {
ERC20BridgeSource,
@ -24,10 +25,9 @@ import {
GetMarketOrdersOpts,
OptimizedMarketOrder,
} from './market_operation_utils/types';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator {
@ -130,15 +130,15 @@ export class SwapQuoteCalculator {
let optimizedOrders: OptimizedMarketOrder[];
let quoteReport: QuoteReport | undefined;
let isTwoHop = false;
let sourceFlags: number = 0;
{
// Scale fees by gas price.
const _opts: GetMarketOrdersOpts = {
...opts,
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
),
exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)),
};
const firstOrderMakerAssetData = !!prunedOrders[0]
@ -157,7 +157,7 @@ export class SwapQuoteCalculator {
);
optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport;
isTwoHop = buyResult.isTwoHop;
sourceFlags = buyResult.sourceFlags;
} else {
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
@ -166,14 +166,14 @@ export class SwapQuoteCalculator {
);
optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport;
isTwoHop = sellResult.isTwoHop;
}
sourceFlags = sellResult.sourceFlags;
}
}
// assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0];
return isTwoHop
const swapQuote =
sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]
? createTwoHopSwapQuote(
makerAssetData,
takerAssetData,
@ -194,6 +194,11 @@ export class SwapQuoteCalculator {
opts.gasSchedule,
quoteReport,
);
// Use the raw gas, not scaled by gas price
const exchangeProxyOverhead = opts.exchangeProxyOverhead(sourceFlags).toNumber();
swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead;
swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead;
return swapQuote;
}
}

View File

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

View File

@ -1,289 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote } from '../src';
import { constants } from '../src/constants';
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const UNLIMITED_ALLOWANCE = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const expectMakerAndTakerBalancesAsyncFactory = (
erc20TokenContract: ERC20TokenContract,
makerAddress: string,
takerAddress: string,
) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => {
const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(expectedMakerBalance);
expect(takerBalance).to.bignumber.equal(expectedTakerBalance);
};
describe('ExchangeSwapQuoteConsumer', () => {
let userAddresses: string[];
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let orderFactory: OrderFactory;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let contractAddresses: ContractAddresses;
let exchangeContract: ExchangeContract;
const chainId = TESTRPC_CHAIN_ID;
let orders: SignedOrderWithFillableAmounts[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ExchangeSwapQuoteConsumer;
let expectMakerAndTakerBalancesForMakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
let expectMakerAndTakerBalancesForTakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
];
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TakerTokenContract,
makerAddress,
takerAddress,
);
expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20MakerTokenContract,
makerAddress,
takerAddress,
);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
orders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
orders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
takerAssetData,
orders,
MarketOperation.Sell,
GAS_PRICE,
);
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
takerAssetData,
orders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
chainId,
});
await erc20MakerTokenContract
.transfer(makerAddress, marketBuySwapQuote.worstCaseQuoteInfo.makerAssetAmount)
.sendTransactionAsync({
from: coinbaseAddress,
});
await erc20TakerTokenContract
.transfer(takerAddress, marketBuySwapQuote.worstCaseQuoteInfo.totalTakerAssetAmount)
.sendTransactionAsync({
from: coinbaseAddress,
});
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: makerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: takerAddress });
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#executeSwapQuoteOrThrowAsync', () => {
/*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
});
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
});
});
describe('#getCalldataOrThrow', () => {
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});
});

View File

@ -1,440 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote } from '../src';
import { constants } from '../src/constants';
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const FEE_PERCENTAGE = 0.05;
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const expectMakerAndTakerBalancesAsyncFactory = (
erc20TokenContract: ERC20TokenContract,
makerAddress: string,
takerAddress: string,
) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => {
const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(expectedMakerBalance);
expect(takerBalance).to.bignumber.equal(expectedTakerBalance);
};
describe('ForwarderSwapQuoteConsumer', () => {
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let orderFactory: OrderFactory;
let invalidOrderFactory: OrderFactory;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let erc20TokenContract: ERC20TokenContract;
let forwarderContract: ForwarderContract;
let orders: SignedOrderWithFillableAmounts[];
let invalidOrders: SignedOrderWithFillableAmounts[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let invalidMarketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ForwarderSwapQuoteConsumer;
let expectMakerAndTakerBalancesAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
const chainId = TESTRPC_CHAIN_ID;
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData: wethAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const invalidDefaultOrderParams = {
...defaultOrderParams,
...{
takerAssetData,
},
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TokenContract,
makerAddress,
takerAddress,
);
invalidOrderFactory = new OrderFactory(privateKey, invalidDefaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
const totalFillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
await erc20TokenContract.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({
from: coinbaseAddress,
});
await erc20TokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: makerAddress });
await forwarderContract.approveMakerAssetProxy(makerAssetData).sendTransactionAsync({ from: makerAddress });
orders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
orders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
invalidOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await invalidOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
invalidOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Sell,
GAS_PRICE,
);
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Buy,
GAS_PRICE,
);
invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
takerAssetData,
invalidOrders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
chainId,
});
swapQuoteConsumer.buyQuoteSellAmountScalingFactor = 1;
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#executeSwapQuoteOrThrowAsync', () => {
describe('validation', () => {
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth)', async () => {
expect(
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }),
).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
});
// TODO(david) test execution of swap quotes with fee orders
describe('valid swap quote', () => {
/*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
gasLimit: 4000000,
ethAmount: new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('should perform a marketBuy execution with affiliate fees', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
gasLimit: 4000000,
extensionContractOpts: {
feePercentage: 0.05,
feeRecipient,
},
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount,
);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
it('should perform a marketSell execution with affiliate fees', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
gasLimit: 4000000,
extensionContractOpts: {
feePercentage: 0.05,
feeRecipient,
},
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount,
);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
});
});
describe('#getCalldataOrThrow', () => {
describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
});
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(forwarderContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{
extensionContractOpts: {
feePercentage: 0.05,
feeRecipient,
},
},
);
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount,
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{
extensionContractOpts: {
feePercentage: 0.05,
feeRecipient,
},
},
);
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount,
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
});
});
// tslint:disable-next-line: max-file-line-count
});

View File

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

View File

@ -19,6 +19,7 @@ export * from '../test/generated-wrappers/i_liquidity_provider_registry';
export * from '../test/generated-wrappers/i_m_stable';
export * from '../test/generated-wrappers/i_mooniswap';
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_v2_router01';
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/native_order_sampler';
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/test_erc20_bridge_sampler';
export * from '../test/generated-wrappers/test_native_order_sampler';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ import * as ethers from 'ethers';
// tslint:enable:no-unused-variable
export type IZeroExEventArgs =
| IZeroExLiquidityProviderForMarketUpdatedEventArgs
| IZeroExMetaTransactionExecutedEventArgs
| IZeroExMigratedEventArgs
| IZeroExOwnershipTransferredEventArgs
@ -45,6 +46,7 @@ export type IZeroExEventArgs =
| IZeroExTransformerDeployerUpdatedEventArgs;
export enum IZeroExEvents {
LiquidityProviderForMarketUpdated = 'LiquidityProviderForMarketUpdated',
MetaTransactionExecuted = 'MetaTransactionExecuted',
Migrated = 'Migrated',
OwnershipTransferred = 'OwnershipTransferred',
@ -54,6 +56,12 @@ export enum IZeroExEvents {
TransformerDeployerUpdated = 'TransformerDeployerUpdated',
}
export interface IZeroExLiquidityProviderForMarketUpdatedEventArgs extends DecodedLogArgs {
xAsset: string;
yAsset: string;
providerAddress: string;
}
export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs {
hash: string;
selector: string;
@ -211,6 +219,29 @@ export class IZeroExContract extends BaseContract {
*/
public static ABI(): ContractAbi {
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,
inputs: [
@ -697,6 +728,27 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
name: 'xAsset',
type: 'address',
},
{
name: 'yAsset',
type: 'address',
},
],
name: 'getLiquidityProviderForMarket',
outputs: [
{
name: 'providerAddress',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
@ -1000,6 +1052,39 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'nonpayable',
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: [
{
@ -1029,6 +1114,26 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
name: 'xAsset',
type: 'address',
},
{
name: 'yAsset',
type: 'address',
},
{
name: 'providerAddress',
type: 'address',
},
],
name: 'setLiquidityProviderForMarket',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
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.
* @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.
* @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.
* Only callable by the owner.

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import * as EncoderMath from '../utils/math';
export class UIntDataType extends AbstractBlobDataType {
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 _MAX_WIDTH: number = 256;

View File

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

View File

@ -0,0 +1,47 @@
import { RevertError } from '../../revert_error';
import { Numberish } from '../../types';
// tslint:disable:max-classes-per-file
export class LiquidityProviderIncompleteSellError extends RevertError {
constructor(
providerAddress?: string,
makerToken?: string,
takerToken?: string,
sellAmount?: Numberish,
boughtAmount?: Numberish,
minBuyAmount?: Numberish,
) {
super(
'LiquidityProviderIncompleteSellError',
'LiquidityProviderIncompleteSellError(address providerAddress, address makerToken, address takerToken, uint256 sellAmount, uint256 boughtAmount, uint256 minBuyAmount)',
{
providerAddress,
makerToken,
takerToken,
sellAmount,
boughtAmount,
minBuyAmount,
},
);
}
}
export class NoLiquidityProviderForMarketError extends RevertError {
constructor(xAsset?: string, yAsset?: string) {
super(
'NoLiquidityProviderForMarketError',
'NoLiquidityProviderForMarketError(address xAsset, address yAsset)',
{
xAsset,
yAsset,
},
);
}
}
const types = [LiquidityProviderIncompleteSellError, NoLiquidityProviderForMarketError];
// Register the types we've defined.
for (const type of types) {
RevertError.registerType(type);
}