MultiplexFeature and BatchFillNativeOrdersFeature (#140)

* WrappedFillFeature

* Address internal feedback

* create features/interfaces/ directory

* Split NativeOrdersFeature into mixins

* Rename mixins to use NativeOrders namespace

* Add BatchFillNativeOrdersFeature

* Rename WrapperFillFeature => MultiplexFeature and add natspec comments

* Emit LiquidityProviderSwap event

* post-rebase fixes

* Multiplex mainnet fork tests

* lint

* Add tests for batch fill functions

* Remove market functions

* Addres PR feedback

* Remove nested _batchFill calls from _multiHopFill

* Add BatchFillIncompleteRevertError type

* Use call{value: amount}() instead of transfer(amount)

* Remove outdated comment

* Update some comments

* Add events

* Address spot-check recommendations

* Remove-top level events, add ExpiredRfqOrder event

* Update changelog

* Change ExpiredRfqOrder event

* Update IZeroEx artifact and contract wrapper
This commit is contained in:
mzhu25 2021-03-08 15:45:49 -08:00 committed by GitHub
parent 22c8e0b6db
commit 3cc639c8d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 5337 additions and 1612 deletions

View File

@ -6,6 +6,7 @@ export {
WETH9Events,
WETH9DepositEventArgs,
WETH9TransferEventArgs,
WETH9WithdrawalEventArgs,
ZRXTokenContract,
DummyERC20TokenTransferEventArgs,
ERC20TokenEventArgs,

View File

@ -9,6 +9,10 @@
{
"note": "Emit `LiquidityProviderFill` event in `CurveLiquidityProvider`",
"pr": 143
},
{
"note": "Add BatchFillNativeOrdersFeature and MultiplexFeature",
"pr": 140
}
]
},

View File

@ -20,14 +20,16 @@
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "./features/IOwnableFeature.sol";
import "./features/ISimpleFunctionRegistryFeature.sol";
import "./features/ITokenSpenderFeature.sol";
import "./features/ITransformERC20Feature.sol";
import "./features/IMetaTransactionsFeature.sol";
import "./features/IUniswapFeature.sol";
import "./features/ILiquidityProviderFeature.sol";
import "./features/INativeOrdersFeature.sol";
import "./features/interfaces/IOwnableFeature.sol";
import "./features/interfaces/ISimpleFunctionRegistryFeature.sol";
import "./features/interfaces/ITokenSpenderFeature.sol";
import "./features/interfaces/ITransformERC20Feature.sol";
import "./features/interfaces/IMetaTransactionsFeature.sol";
import "./features/interfaces/IUniswapFeature.sol";
import "./features/interfaces/ILiquidityProviderFeature.sol";
import "./features/interfaces/INativeOrdersFeature.sol";
import "./features/interfaces/IBatchFillNativeOrdersFeature.sol";
import "./features/interfaces/IMultiplexFeature.sol";
/// @dev Interface for a fully featured Exchange Proxy.
@ -39,7 +41,9 @@ interface IZeroEx is
IMetaTransactionsFeature,
IUniswapFeature,
ILiquidityProviderFeature,
INativeOrdersFeature
INativeOrdersFeature,
IBatchFillNativeOrdersFeature,
IMultiplexFeature
{
// solhint-disable state-visibility

View File

@ -170,4 +170,21 @@ library LibNativeOrdersRichErrors {
maker
);
}
function BatchFillIncompleteError(
bytes32 orderHash,
uint256 takerTokenFilledAmount,
uint256 takerTokenFillAmount
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("BatchFillIncompleteError(bytes32,uint256,uint256)")),
orderHash,
takerTokenFilledAmount,
takerTokenFillAmount
);
}
}

View File

@ -0,0 +1,198 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../errors/LibNativeOrdersRichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../fixins/FixinEIP712.sol";
import "../migrations/LibMigrate.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/IBatchFillNativeOrdersFeature.sol";
import "./interfaces/INativeOrdersFeature.sol";
import "./libs/LibNativeOrder.sol";
import "./libs/LibSignature.sol";
/// @dev Feature for batch/market filling limit and RFQ orders.
contract BatchFillNativeOrdersFeature is
IFeature,
IBatchFillNativeOrdersFeature,
FixinCommon,
FixinEIP712
{
using LibSafeMathV06 for uint128;
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "BatchFill";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
constructor(address zeroExAddress)
public
FixinEIP712(zeroExAddress)
{
// solhint-disable no-empty-blocks
}
/// @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.batchFillLimitOrders.selector);
_registerFeatureFunction(this.batchFillRfqOrders.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Fills multiple limit orders.
/// @param orders Array of limit orders.
/// @param signatures Array of signatures corresponding to each order.
/// @param takerTokenFillAmounts Array of desired amounts to fill each order.
/// @param revertIfIncomplete If true, reverts if this function fails to
/// fill the full fill amount for any individual order.
/// @return takerTokenFilledAmounts Array of amounts filled, in taker token.
/// @return makerTokenFilledAmounts Array of amounts filled, in maker token.
function batchFillLimitOrders(
LibNativeOrder.LimitOrder[] calldata orders,
LibSignature.Signature[] calldata signatures,
uint128[] calldata takerTokenFillAmounts,
bool revertIfIncomplete
)
external
payable
override
returns (
uint128[] memory takerTokenFilledAmounts,
uint128[] memory makerTokenFilledAmounts
)
{
require(
orders.length == signatures.length && orders.length == takerTokenFillAmounts.length,
'BatchFillNativeOrdersFeature::batchFillLimitOrders/MISMATCHED_ARRAY_LENGTHS'
);
takerTokenFilledAmounts = new uint128[](orders.length);
makerTokenFilledAmounts = new uint128[](orders.length);
uint256 protocolFee = uint256(INativeOrdersFeature(address(this)).getProtocolFeeMultiplier())
.safeMul(tx.gasprice);
uint256 ethProtocolFeePaid;
for (uint256 i = 0; i != orders.length; i++) {
try
INativeOrdersFeature(address(this))._fillLimitOrder
(
orders[i],
signatures[i],
takerTokenFillAmounts[i],
msg.sender,
msg.sender
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Update amounts filled.
(takerTokenFilledAmounts[i], makerTokenFilledAmounts[i]) =
(takerTokenFilledAmount, makerTokenFilledAmount);
ethProtocolFeePaid = ethProtocolFeePaid.safeAdd(protocolFee);
} catch {}
if (
revertIfIncomplete &&
takerTokenFilledAmounts[i] < takerTokenFillAmounts[i]
) {
bytes32 orderHash = _getEIP712Hash(
LibNativeOrder.getLimitOrderStructHash(orders[i])
);
// Did not fill the amount requested.
LibNativeOrdersRichErrors.BatchFillIncompleteError(
orderHash,
takerTokenFilledAmounts[i],
takerTokenFillAmounts[i]
).rrevert();
}
}
LibNativeOrder.refundExcessProtocolFeeToSender(ethProtocolFeePaid);
}
/// @dev Fills multiple RFQ orders.
/// @param orders Array of RFQ orders.
/// @param signatures Array of signatures corresponding to each order.
/// @param takerTokenFillAmounts Array of desired amounts to fill each order.
/// @param revertIfIncomplete If true, reverts if this function fails to
/// fill the full fill amount for any individual order.
/// @return takerTokenFilledAmounts Array of amounts filled, in taker token.
/// @return makerTokenFilledAmounts Array of amounts filled, in maker token.
function batchFillRfqOrders(
LibNativeOrder.RfqOrder[] calldata orders,
LibSignature.Signature[] calldata signatures,
uint128[] calldata takerTokenFillAmounts,
bool revertIfIncomplete
)
external
override
returns (
uint128[] memory takerTokenFilledAmounts,
uint128[] memory makerTokenFilledAmounts
)
{
require(
orders.length == signatures.length && orders.length == takerTokenFillAmounts.length,
'BatchFillNativeOrdersFeature::batchFillRfqOrders/MISMATCHED_ARRAY_LENGTHS'
);
takerTokenFilledAmounts = new uint128[](orders.length);
makerTokenFilledAmounts = new uint128[](orders.length);
for (uint256 i = 0; i != orders.length; i++) {
try
INativeOrdersFeature(address(this))._fillRfqOrder
(
orders[i],
signatures[i],
takerTokenFillAmounts[i],
msg.sender
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Update amounts filled.
(takerTokenFilledAmounts[i], makerTokenFilledAmounts[i]) =
(takerTokenFilledAmount, makerTokenFilledAmount);
} catch {}
if (
revertIfIncomplete &&
takerTokenFilledAmounts[i] < takerTokenFillAmounts[i]
) {
// Did not fill the amount requested.
bytes32 orderHash = _getEIP712Hash(
LibNativeOrder.getRfqOrderStructHash(orders[i])
);
LibNativeOrdersRichErrors.BatchFillIncompleteError(
orderHash,
takerTokenFilledAmounts[i],
takerTokenFillAmounts[i]
).rrevert();
}
}
}
}

View File

@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../migrations/LibBootstrap.sol";
import "../storage/LibProxyStorage.sol";
import "./IBootstrapFeature.sol";
import "./interfaces/IBootstrapFeature.sol";
/// @dev Detachable `bootstrap()` feature.

View File

@ -31,8 +31,8 @@ import "../fixins/FixinCommon.sol";
import "../fixins/FixinTokenSpender.sol";
import "../migrations/LibMigrate.sol";
import "../transformers/LibERC20Transformer.sol";
import "./IFeature.sol";
import "./ILiquidityProviderFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/ILiquidityProviderFeature.sol";
contract LiquidityProviderFeature is

View File

@ -30,11 +30,11 @@ import "../fixins/FixinTokenSpender.sol";
import "../fixins/FixinEIP712.sol";
import "../migrations/LibMigrate.sol";
import "../storage/LibMetaTransactionsStorage.sol";
import "./IMetaTransactionsFeature.sol";
import "./ITransformERC20Feature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/IMetaTransactionsFeature.sol";
import "./interfaces/INativeOrdersFeature.sol";
import "./interfaces/ITransformERC20Feature.sol";
import "./libs/LibSignature.sol";
import "./IFeature.sol";
import "./INativeOrdersFeature.sol";
/// @dev MetaTransactions feature.
contract MetaTransactionsFeature is

View File

@ -0,0 +1,805 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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-utils/contracts/src/v06/LibMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../external/ILiquidityProviderSandbox.sol";
import "../fixins/FixinCommon.sol";
import "../fixins/FixinEIP712.sol";
import "../fixins/FixinTokenSpender.sol";
import "../migrations/LibMigrate.sol";
import "../transformers/LibERC20Transformer.sol";
import "../vendor/ILiquidityProvider.sol";
import "../vendor/IUniswapV2Pair.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/IMultiplexFeature.sol";
import "./interfaces/INativeOrdersFeature.sol";
import "./interfaces/ITransformERC20Feature.sol";
import "./libs/LibNativeOrder.sol";
/// @dev This feature enables efficient batch and multi-hop trades
/// using different liquidity sources.
contract MultiplexFeature is
IFeature,
IMultiplexFeature,
FixinCommon,
FixinEIP712,
FixinTokenSpender
{
using LibERC20Transformer for IERC20TokenV06;
using LibSafeMathV06 for uint128;
using LibSafeMathV06 for uint256;
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "MultiplexFeature";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev The WETH token contract.
IEtherTokenV06 private immutable weth;
/// @dev The sandbox contract address.
ILiquidityProviderSandbox public immutable sandbox;
// address of the UniswapV2Factory contract.
address private constant UNISWAP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
// address of the (Sushiswap) UniswapV2Factory contract.
address private constant SUSHISWAP_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac;
// Init code hash of the UniswapV2Pair contract.
uint256 private constant UNISWAP_PAIR_INIT_CODE_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
// Init code hash of the (Sushiswap) UniswapV2Pair contract.
uint256 private constant SUSHISWAP_PAIR_INIT_CODE_HASH = 0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303;
constructor(
address zeroExAddress,
IEtherTokenV06 weth_,
ILiquidityProviderSandbox sandbox_,
bytes32 greedyTokensBloomFilter
)
public
FixinEIP712(zeroExAddress)
FixinTokenSpender(greedyTokensBloomFilter)
{
weth = weth_;
sandbox = sandbox_;
}
/// @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.batchFill.selector);
_registerFeatureFunction(this.multiHopFill.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Executes a batch of fills selling `fillData.inputToken`
/// for `fillData.outputToken` in sequence. Refer to the
/// internal variant `_batchFill` for the allowed nested
/// operations.
/// @param fillData Encodes the input/output tokens, the sell
/// amount, and the nested operations for this batch fill.
/// @param minBuyAmount The minimum amount of `fillData.outputToken`
/// to buy. Reverts if this amount is not met.
/// @return outputTokenAmount The amount of the output token bought.
function batchFill(
BatchFillData memory fillData,
uint256 minBuyAmount
)
public
payable
override
returns (uint256 outputTokenAmount)
{
// Cache the sender's balance of the output token.
outputTokenAmount = fillData.outputToken.getTokenBalanceOf(msg.sender);
// Cache the contract's ETH balance prior to this call.
uint256 ethBalanceBefore = address(this).balance.safeSub(msg.value);
// Perform the batch fill.
_batchFill(fillData);
// The `outputTokenAmount` returned by `_batchFill` may not
// be fully accurate (e.g. due to some janky token).
outputTokenAmount = fillData.outputToken.getTokenBalanceOf(msg.sender)
.safeSub(outputTokenAmount);
require(
outputTokenAmount >= minBuyAmount,
"MultiplexFeature::batchFill/UNDERBOUGHT"
);
uint256 ethBalanceAfter = address(this).balance;
require(
ethBalanceAfter >= ethBalanceBefore,
"MultiplexFeature::batchFill/OVERSPENT_ETH"
);
// Refund ETH
if (ethBalanceAfter > ethBalanceBefore) {
_transferEth(msg.sender, ethBalanceAfter - ethBalanceBefore);
}
}
/// @dev Executes a sequence of fills "hopping" through the
/// path of tokens given by `fillData.tokens`. Refer to the
/// internal variant `_multiHopFill` for the allowed nested
/// operations.
/// @param fillData Encodes the path of tokens, the sell amount,
/// and the nested operations for this multi-hop fill.
/// @param minBuyAmount The minimum amount of the output token
/// to buy. Reverts if this amount is not met.
/// @return outputTokenAmount The amount of the output token bought.
function multiHopFill(
MultiHopFillData memory fillData,
uint256 minBuyAmount
)
public
payable
override
returns (uint256 outputTokenAmount)
{
IERC20TokenV06 outputToken = IERC20TokenV06(fillData.tokens[fillData.tokens.length - 1]);
// Cache the sender's balance of the output token.
outputTokenAmount = outputToken.getTokenBalanceOf(msg.sender);
// Cache the contract's ETH balance prior to this call.
uint256 ethBalanceBefore = address(this).balance.safeSub(msg.value);
// Perform the multi-hop fill. Pass in `msg.value` as the maximum
// allowable amount of ETH for the wrapped calls to consume.
_multiHopFill(fillData, msg.value);
// The `outputTokenAmount` returned by `_multiHopFill` may not
// be fully accurate (e.g. due to some janky token).
outputTokenAmount = outputToken.getTokenBalanceOf(msg.sender)
.safeSub(outputTokenAmount);
require(
outputTokenAmount >= minBuyAmount,
"MultiplexFeature::multiHopFill/UNDERBOUGHT"
);
uint256 ethBalanceAfter = address(this).balance;
require(
ethBalanceAfter >= ethBalanceBefore,
"MultiplexFeature::multiHopFill/OVERSPENT_ETH"
);
// Refund ETH
if (ethBalanceAfter > ethBalanceBefore) {
_transferEth(msg.sender, ethBalanceAfter - ethBalanceBefore);
}
}
// Similar to FQT. If `fillData.sellAmount` is set to `type(uint256).max`,
// this is effectively a batch fill. Otherwise it can be set to perform a
// market sell of some amount. Note that the `outputTokenAmount` returned
// by this function could theoretically be inaccurate if `msg.sender` has
// set a token allowance on an external contract that gets called during
// the execution of this function.
function _batchFill(BatchFillData memory fillData)
internal
returns (uint256 outputTokenAmount, uint256 remainingEth)
{
// Track the remaining ETH allocated to this call.
remainingEth = msg.value;
// Track the amount of input token sold.
uint256 soldAmount;
for (uint256 i = 0; i != fillData.calls.length; i++) {
// Check if we've hit our target.
if (soldAmount >= fillData.sellAmount) { break; }
WrappedBatchCall memory wrappedCall = fillData.calls[i];
// Compute the fill amount.
uint256 inputTokenAmount = LibSafeMathV06.min256(
wrappedCall.sellAmount,
fillData.sellAmount.safeSub(soldAmount)
);
if (wrappedCall.selector == INativeOrdersFeature._fillRfqOrder.selector) {
// Decode the RFQ order and signature.
(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature
) = abi.decode(
wrappedCall.data,
(LibNativeOrder.RfqOrder, LibSignature.Signature)
);
if (order.expiry <= uint64(block.timestamp)) {
bytes32 orderHash = _getEIP712Hash(
LibNativeOrder.getRfqOrderStructHash(order)
);
emit ExpiredRfqOrder(
orderHash,
order.maker,
order.expiry
);
continue;
}
require(
order.takerToken == fillData.inputToken &&
order.makerToken == fillData.outputToken,
"MultiplexFeature::_batchFill/RFQ_ORDER_INVALID_TOKENS"
);
// Try filling the RFQ order. Swallows reverts.
try
INativeOrdersFeature(address(this))._fillRfqOrder
(
order,
signature,
inputTokenAmount.safeDowncastToUint128(),
msg.sender
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Increment the sold and bought amounts.
soldAmount = soldAmount.safeAdd(takerTokenFilledAmount);
outputTokenAmount = outputTokenAmount.safeAdd(makerTokenFilledAmount);
} catch {}
} else if (wrappedCall.selector == this._sellToUniswap.selector) {
(address[] memory tokens, bool isSushi) = abi.decode(
wrappedCall.data,
(address[], bool)
);
require(
tokens.length >= 2 &&
tokens[0] == address(fillData.inputToken) &&
tokens[tokens.length - 1] == address(fillData.outputToken),
"MultiplexFeature::_batchFill/UNISWAP_INVALID_TOKENS"
);
// Perform the Uniswap/Sushiswap trade.
uint256 outputTokenAmount_ = _sellToUniswap(
tokens,
inputTokenAmount,
isSushi,
address(0),
msg.sender
);
// Increment the sold and bought amounts.
soldAmount = soldAmount.safeAdd(inputTokenAmount);
outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_);
} else if (wrappedCall.selector == this._sellToLiquidityProvider.selector) {
(address provider, bytes memory auxiliaryData) = abi.decode(
wrappedCall.data,
(address, bytes)
);
if (fillData.inputToken.isTokenETH()) {
inputTokenAmount = LibSafeMathV06.min256(
inputTokenAmount,
remainingEth
);
// Transfer the input ETH to the provider.
_transferEth(payable(provider), inputTokenAmount);
// Count that ETH as spent.
remainingEth -= inputTokenAmount;
} else {
// Transfer input ERC20 tokens to the provider.
_transferERC20Tokens(
fillData.inputToken,
msg.sender,
provider,
inputTokenAmount
);
}
// Perform the PLP trade.
uint256 outputTokenAmount_ = _sellToLiquidityProvider(
fillData.inputToken,
fillData.outputToken,
inputTokenAmount,
ILiquidityProvider(provider),
msg.sender,
auxiliaryData
);
// Increment the sold and bought amounts.
soldAmount = soldAmount.safeAdd(inputTokenAmount);
outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_);
} else if (wrappedCall.selector == ITransformERC20Feature._transformERC20.selector) {
ITransformERC20Feature.TransformERC20Args memory args;
args.taker = msg.sender;
args.inputToken = fillData.inputToken;
args.outputToken = fillData.outputToken;
args.inputTokenAmount = inputTokenAmount;
args.minOutputTokenAmount = 0;
uint256 ethValue;
(args.transformations, ethValue) = abi.decode(
wrappedCall.data,
(ITransformERC20Feature.Transformation[], uint256)
);
// Do not spend more than the remaining ETH.
ethValue = LibSafeMathV06.min256(
ethValue,
remainingEth
);
if (ethValue > 0) {
require(
args.inputToken.isTokenETH(),
"MultiplexFeature::_batchFill/ETH_TRANSFORM_ONLY"
);
}
try ITransformERC20Feature(address(this))._transformERC20
{value: ethValue}
(args)
returns (uint256 outputTokenAmount_)
{
remainingEth -= ethValue;
soldAmount = soldAmount.safeAdd(inputTokenAmount);
outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_);
} catch {}
} else if (wrappedCall.selector == this._multiHopFill.selector) {
MultiHopFillData memory multiHopFillData;
uint256 ethValue;
(
multiHopFillData.tokens,
multiHopFillData.calls,
ethValue
) = abi.decode(
wrappedCall.data,
(address[], WrappedMultiHopCall[], uint256)
);
multiHopFillData.sellAmount = inputTokenAmount;
// Do not spend more than the remaining ETH.
ethValue = LibSafeMathV06.min256(
ethValue,
remainingEth
);
// Subtract the ethValue allocated to the nested multi-hop fill.
remainingEth -= ethValue;
(uint256 outputTokenAmount_, uint256 leftoverEth) =
_multiHopFill(multiHopFillData, ethValue);
// Increment the sold and bought amounts.
soldAmount = soldAmount.safeAdd(inputTokenAmount);
outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_);
// Add back any ETH that wasn't used by the nested multi-hop fill.
remainingEth += leftoverEth;
} else {
revert("MultiplexFeature::_batchFill/UNRECOGNIZED_SELECTOR");
}
}
}
// Internal variant of `multiHopFill`. This function can be nested within
// a `_batchFill`.
// This function executes a sequence of fills "hopping" through the
// path of tokens given by `fillData.tokens`. The nested operations that
// can be used as "hops" are:
// - WETH.deposit (wraps ETH)
// - WETH.withdraw (unwraps WETH)
// - _sellToUniswap (executes a Uniswap/Sushiswap swap)
// - _sellToLiquidityProvider (executes a PLP swap)
// - _transformERC20 (executes arbitrary ERC20 Transformations)
// This function optimizes the number of ERC20 transfers performed
// by having each hop transfer its output tokens directly to the
// target address of the next hop. Note that the `outputTokenAmount` returned
// by this function could theoretically be inaccurate if `msg.sender` has
// set a token allowance on an external contract that gets called during
// the execution of this function.
function _multiHopFill(MultiHopFillData memory fillData, uint256 totalEth)
public
returns (uint256 outputTokenAmount, uint256 remainingEth)
{
// There should be one call/hop between every two tokens
// in the path.
// tokens[0]calls[0]>tokens[1]...calls[n-1]>tokens[n]
require(
fillData.tokens.length == fillData.calls.length + 1,
"MultiplexFeature::_multiHopFill/MISMATCHED_ARRAY_LENGTHS"
);
// Track the remaining ETH allocated to this call.
remainingEth = totalEth;
// This variable is used as the input and output amounts of
// each hop. After the final hop, this will contain the output
// amount of the multi-hop fill.
outputTokenAmount = fillData.sellAmount;
// This variable is used to cache the address to target in the
// next hop. See `_computeHopRecipient` for details.
address nextTarget;
for (uint256 i = 0; i != fillData.calls.length; i++) {
WrappedMultiHopCall memory wrappedCall = fillData.calls[i];
if (wrappedCall.selector == this._sellToUniswap.selector) {
// If the next hop supports a "transfer then execute" pattern,
// the recipient will not be `msg.sender`. See `_computeHopRecipient`
// for details.
address recipient = _computeHopRecipient(fillData.calls, i);
(address[] memory tokens, bool isSushi) = abi.decode(
wrappedCall.data,
(address[], bool)
);
// Perform the Uniswap/Sushiswap trade.
outputTokenAmount = _sellToUniswap(
tokens,
outputTokenAmount,
isSushi,
nextTarget,
recipient
);
// If the recipient was not `msg.sender`, it must be the target
// contract for the next hop.
nextTarget = recipient == msg.sender ? address(0) : recipient;
} else if (wrappedCall.selector == this._sellToLiquidityProvider.selector) {
// If the next hop supports a "transfer then execute" pattern,
// the recipient will not be `msg.sender`. See `_computeHopRecipient`
// for details.
address recipient = _computeHopRecipient(fillData.calls, i);
// If `nextTarget` was not set in the previous hop, then we
// need to send in the input ETH/tokens to the liquidity provider
// contract before executing the trade.
if (nextTarget == address(0)) {
(address provider, bytes memory auxiliaryData) = abi.decode(
wrappedCall.data,
(address, bytes)
);
// Transfer input ETH or ERC20 tokens to the liquidity
// provider contract.
if (IERC20TokenV06(fillData.tokens[i]).isTokenETH()) {
outputTokenAmount = LibSafeMathV06.min256(
outputTokenAmount,
remainingEth
);
_transferEth(payable(provider), outputTokenAmount);
remainingEth -= outputTokenAmount;
} else {
_transferERC20Tokens(
IERC20TokenV06(fillData.tokens[i]),
msg.sender,
provider,
outputTokenAmount
);
}
outputTokenAmount = _sellToLiquidityProvider(
IERC20TokenV06(fillData.tokens[i]),
IERC20TokenV06(fillData.tokens[i + 1]),
outputTokenAmount,
ILiquidityProvider(provider),
recipient,
auxiliaryData
);
} else {
(, bytes memory auxiliaryData) = abi.decode(
wrappedCall.data,
(address, bytes)
);
// Tokens and ETH have already been transferred to
// the liquidity provider contract in the previous hop.
outputTokenAmount = _sellToLiquidityProvider(
IERC20TokenV06(fillData.tokens[i]),
IERC20TokenV06(fillData.tokens[i + 1]),
outputTokenAmount,
ILiquidityProvider(nextTarget),
recipient,
auxiliaryData
);
}
// If the recipient was not `msg.sender`, it must be the target
// contract for the next hop.
nextTarget = recipient == msg.sender ? address(0) : recipient;
} else if (wrappedCall.selector == ITransformERC20Feature._transformERC20.selector) {
ITransformERC20Feature.TransformERC20Args memory args;
args.inputToken = IERC20TokenV06(fillData.tokens[i]);
args.outputToken = IERC20TokenV06(fillData.tokens[i + 1]);
args.minOutputTokenAmount = 0;
args.taker = payable(_computeHopRecipient(fillData.calls, i));
if (nextTarget != address(0)) {
// If `nextTarget` was set in the previous hop, then the input
// token was already sent to the FlashWallet. Setting
// `inputTokenAmount` to 0 indicates that no tokens need to
// be pulled into the FlashWallet before executing the
// transformations.
args.inputTokenAmount = 0;
} else if (
args.taker != msg.sender &&
!args.inputToken.isTokenETH()
) {
address flashWallet = address(
ITransformERC20Feature(address(this)).getTransformWallet()
);
// The input token has _not_ already been sent to the
// FlashWallet. We also want PayTakerTransformer to
// send the output token to some address other than
// msg.sender, so we must transfer the input token
// to the FlashWallet here.
_transferERC20Tokens(
args.inputToken,
msg.sender,
flashWallet,
outputTokenAmount
);
args.inputTokenAmount = 0;
} else {
// Otherwise, either:
// (1) args.taker == msg.sender, in which case
// `_transformERC20` will pull the input token
// into the FlashWallet, or
// (2) args.inputToken == ETH_TOKEN_ADDRESS, in which
// case ETH is attached to the call and no token
// transfer occurs.
args.inputTokenAmount = outputTokenAmount;
}
uint256 ethValue;
(args.transformations, ethValue) = abi.decode(
wrappedCall.data,
(ITransformERC20Feature.Transformation[], uint256)
);
// Do not spend more than the remaining ETH.
ethValue = LibSafeMathV06.min256(ethValue, remainingEth);
if (ethValue > 0) {
require(
args.inputToken.isTokenETH(),
"MultiplexFeature::_multiHopFill/ETH_TRANSFORM_ONLY"
);
}
// Call `_transformERC20`.
outputTokenAmount = ITransformERC20Feature(address(this))
._transformERC20{value: ethValue}(args);
// Decrement the remaining ETH.
remainingEth -= ethValue;
// If the recipient was not `msg.sender`, it must be the target
// contract for the next hop.
nextTarget = args.taker == msg.sender ? address(0) : args.taker;
} else if (wrappedCall.selector == IEtherTokenV06.deposit.selector) {
require(
i == 0,
"MultiplexFeature::_multiHopFill/DEPOSIT_FIRST_HOP_ONLY"
);
uint256 ethValue = LibSafeMathV06.min256(outputTokenAmount, remainingEth);
// Wrap ETH.
weth.deposit{value: ethValue}();
nextTarget = _computeHopRecipient(fillData.calls, i);
weth.transfer(nextTarget, ethValue);
remainingEth -= ethValue;
} else if (wrappedCall.selector == IEtherTokenV06.withdraw.selector) {
require(
i == fillData.calls.length - 1,
"MultiplexFeature::_multiHopFill/WITHDRAW_LAST_HOP_ONLY"
);
// Unwrap WETH and send to `msg.sender`.
weth.withdraw(outputTokenAmount);
_transferEth(msg.sender, outputTokenAmount);
nextTarget = address(0);
} else {
revert("MultiplexFeature::_multiHopFill/UNRECOGNIZED_SELECTOR");
}
}
}
// Similar to the UniswapFeature, but with a couple of differences:
// - Does not perform the transfer in if `pairAddress` is given,
// which indicates that the transfer in was already performed
// in the previous hop of a multi-hop fill.
// - Does not include a minBuyAmount check (which is performed in
// either `batchFill` or `multiHopFill`).
// - Takes a `recipient` address parameter, so the output of the
// final `swap` call can be sent to an address other than `msg.sender`.
function _sellToUniswap(
address[] memory tokens,
uint256 sellAmount,
bool isSushi,
address pairAddress,
address recipient
)
public
returns (uint256 outputTokenAmount)
{
require(tokens.length > 1, "MultiplexFeature::_sellToUniswap/InvalidTokensLength");
if (pairAddress == address(0)) {
pairAddress = _computeUniswapPairAddress(tokens[0], tokens[1], isSushi);
_transferERC20Tokens(
IERC20TokenV06(tokens[0]),
msg.sender,
pairAddress,
sellAmount
);
}
for (uint256 i = 0; i < tokens.length - 1; i++) {
(address inputToken, address outputToken) = (tokens[i], tokens[i + 1]);
outputTokenAmount = _computeUniswapOutputAmount(
pairAddress,
inputToken,
outputToken,
sellAmount
);
(uint256 amount0Out, uint256 amount1Out) = inputToken < outputToken
? (uint256(0), outputTokenAmount)
: (outputTokenAmount, uint256(0));
address to = i < tokens.length - 2
? _computeUniswapPairAddress(outputToken, tokens[i + 2], isSushi)
: recipient;
IUniswapV2Pair(pairAddress).swap(
amount0Out,
amount1Out,
to,
new bytes(0)
);
pairAddress = to;
sellAmount = outputTokenAmount;
}
}
// Same as the LiquidityProviderFeature, but without the transfer in
// (which is potentially done in the previous hop of a multi-hop fill)
// and without the minBuyAmount check (which is performed at the top, i.e.
// in either `batchFill` or `multiHopFill`).
function _sellToLiquidityProvider(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
ILiquidityProvider provider,
address recipient,
bytes memory auxiliaryData
)
public
returns (uint256 outputTokenAmount)
{
uint256 balanceBefore = IERC20TokenV06(outputToken).getTokenBalanceOf(recipient);
if (IERC20TokenV06(inputToken).isTokenETH()) {
sandbox.executeSellEthForToken(
provider,
outputToken,
recipient,
0,
auxiliaryData
);
} else if (IERC20TokenV06(outputToken).isTokenETH()) {
sandbox.executeSellTokenForEth(
provider,
inputToken,
recipient,
0,
auxiliaryData
);
} else {
sandbox.executeSellTokenForToken(
provider,
inputToken,
outputToken,
recipient,
0,
auxiliaryData
);
}
outputTokenAmount = IERC20TokenV06(outputToken).getTokenBalanceOf(recipient)
.safeSub(balanceBefore);
emit LiquidityProviderSwap(
address(inputToken),
address(outputToken),
inputTokenAmount,
outputTokenAmount,
address(provider),
recipient
);
return outputTokenAmount;
}
function _transferEth(address payable recipient, uint256 amount)
private
{
(bool success,) = recipient.call{value: amount}("");
require(success, "MultiplexFeature::_transferEth/TRANSFER_FALIED");
}
// Some liquidity sources (e.g. Uniswap, Sushiswap, and PLP) can be passed
// a `recipient` parameter so the boguht tokens are transferred to the
// `recipient` address rather than `msg.sender`.
// Some liquidity sources (also Uniswap, Sushiswap, and PLP incidentally)
// support a "transfer then execute" pattern, where the token being sold
// can be transferred into the contract before calling a swap function to
// execute the trade.
// If the current hop in a multi-hop fill satisfies the first condition,
// and the next hop satisfies the second condition, the tokens bought
// in the current hop can be directly sent to the target contract of
// the next hop to save a transfer.
function _computeHopRecipient(
WrappedMultiHopCall[] memory calls,
uint256 i
)
private
view
returns (address recipient)
{
recipient = msg.sender;
if (i < calls.length - 1) {
WrappedMultiHopCall memory nextCall = calls[i + 1];
if (nextCall.selector == this._sellToUniswap.selector) {
(address[] memory tokens, bool isSushi) = abi.decode(
nextCall.data,
(address[], bool)
);
recipient = _computeUniswapPairAddress(tokens[0], tokens[1], isSushi);
} else if (nextCall.selector == this._sellToLiquidityProvider.selector) {
(recipient,) = abi.decode(
nextCall.data,
(address, bytes)
);
} else if (nextCall.selector == IEtherTokenV06.withdraw.selector) {
recipient = address(this);
} else if (nextCall.selector == ITransformERC20Feature._transformERC20.selector) {
recipient = address(
ITransformERC20Feature(address(this)).getTransformWallet()
);
}
}
require(
recipient != address(0),
"MultiplexFeature::_computeHopRecipient/RECIPIENT_IS_NULL"
);
}
// Computes the the amount of output token that would be bought
// from Uniswap/Sushiswap given the input amount.
function _computeUniswapOutputAmount(
address pairAddress,
address inputToken,
address outputToken,
uint256 inputAmount
)
private
view
returns (uint256 outputAmount)
{
require(
inputAmount > 0,
"MultiplexFeature::_computeUniswapOutputAmount/INSUFFICIENT_INPUT_AMOUNT"
);
(uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairAddress).getReserves();
require(
reserve0 > 0 && reserve1 > 0,
'MultiplexFeature::_computeUniswapOutputAmount/INSUFFICIENT_LIQUIDITY'
);
(uint256 inputReserve, uint256 outputReserve) = inputToken < outputToken
? (reserve0, reserve1)
: (reserve1, reserve0);
uint256 inputAmountWithFee = inputAmount.safeMul(997);
uint256 numerator = inputAmountWithFee.safeMul(outputReserve);
uint256 denominator = inputReserve.safeMul(1000).safeAdd(inputAmountWithFee);
return numerator / denominator;
}
// Computes the Uniswap/Sushiswap pair contract address for the
// given tokens.
function _computeUniswapPairAddress(
address tokenA,
address tokenB,
bool isSushi
)
private
pure
returns (address pairAddress)
{
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
if (isSushi) {
return address(uint256(keccak256(abi.encodePacked(
hex'ff',
SUSHISWAP_FACTORY,
keccak256(abi.encodePacked(token0, token1)),
SUSHISWAP_PAIR_INIT_CODE_HASH
))));
} else {
return address(uint256(keccak256(abi.encodePacked(
hex'ff',
UNISWAP_FACTORY,
keccak256(abi.encodePacked(token0, token1)),
UNISWAP_PAIR_INIT_CODE_HASH
))));
}
}
}

View File

@ -26,8 +26,8 @@ import "../errors/LibOwnableRichErrors.sol";
import "../storage/LibOwnableStorage.sol";
import "../migrations/LibBootstrap.sol";
import "../migrations/LibMigrate.sol";
import "./IFeature.sol";
import "./IOwnableFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/IOwnableFeature.sol";
import "./SimpleFunctionRegistryFeature.sol";

View File

@ -26,8 +26,8 @@ import "../storage/LibProxyStorage.sol";
import "../storage/LibSimpleFunctionRegistryStorage.sol";
import "../errors/LibSimpleFunctionRegistryRichErrors.sol";
import "../migrations/LibBootstrap.sol";
import "./IFeature.sol";
import "./ISimpleFunctionRegistryFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/ISimpleFunctionRegistryFeature.sol";
/// @dev Basic registry management features.

View File

@ -29,8 +29,8 @@ import "../fixins/FixinCommon.sol";
import "../migrations/LibMigrate.sol";
import "../external/IAllowanceTarget.sol";
import "../storage/LibTokenSpenderStorage.sol";
import "./ITokenSpenderFeature.sol";
import "./IFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/ITokenSpenderFeature.sol";
/// @dev Feature that allows spending token allowances.

View File

@ -33,8 +33,8 @@ import "../external/FlashWallet.sol";
import "../storage/LibTransformERC20Storage.sol";
import "../transformers/IERC20Transformer.sol";
import "../transformers/LibERC20Transformer.sol";
import "./ITransformERC20Feature.sol";
import "./IFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/ITransformERC20Feature.sol";
/// @dev Feature to composably transform between ERC20 tokens.
@ -313,7 +313,7 @@ contract TransformERC20Feature is
to.transfer(msg.value);
}
// Transfer input tokens.
if (!LibERC20Transformer.isTokenETH(inputToken)) {
if (!LibERC20Transformer.isTokenETH(inputToken) && amount != 0) {
// Token is not ETH, so pull ERC20 tokens.
_transferERC20Tokens(
inputToken,

View File

@ -25,8 +25,8 @@ import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "../migrations/LibMigrate.sol";
import "../external/IAllowanceTarget.sol";
import "../fixins/FixinCommon.sol";
import "./IFeature.sol";
import "./IUniswapFeature.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/IUniswapFeature.sol";
/// @dev VIP uniswap fill functions.
@ -380,7 +380,7 @@ contract UniswapFeature is
// will eat all our gas.
if isTokenPossiblyGreedy(token) {
// Check if we have enough direct allowance by calling
// `token.allowance()``
// `token.allowance()`
mstore(0xB00, ALLOWANCE_CALL_SELECTOR_32)
mstore(0xB04, caller())
mstore(0xB24, address())

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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 "../libs/LibNativeOrder.sol";
import "../libs/LibSignature.sol";
/// @dev Feature for batch/market filling limit and RFQ orders.
interface IBatchFillNativeOrdersFeature {
/// @dev Fills multiple limit orders.
/// @param orders Array of limit orders.
/// @param signatures Array of signatures corresponding to each order.
/// @param takerTokenFillAmounts Array of desired amounts to fill each order.
/// @param revertIfIncomplete If true, reverts if this function fails to
/// fill the full fill amount for any individual order.
/// @return takerTokenFilledAmounts Array of amounts filled, in taker token.
/// @return makerTokenFilledAmounts Array of amounts filled, in maker token.
function batchFillLimitOrders(
LibNativeOrder.LimitOrder[] calldata orders,
LibSignature.Signature[] calldata signatures,
uint128[] calldata takerTokenFillAmounts,
bool revertIfIncomplete
)
external
payable
returns (
uint128[] memory takerTokenFilledAmounts,
uint128[] memory makerTokenFilledAmounts
);
/// @dev Fills multiple RFQ orders.
/// @param orders Array of RFQ orders.
/// @param signatures Array of signatures corresponding to each order.
/// @param takerTokenFillAmounts Array of desired amounts to fill each order.
/// @param revertIfIncomplete If true, reverts if this function fails to
/// fill the full fill amount for any individual order.
/// @return takerTokenFilledAmounts Array of amounts filled, in taker token.
/// @return makerTokenFilledAmounts Array of amounts filled, in maker token.
function batchFillRfqOrders(
LibNativeOrder.RfqOrder[] calldata orders,
LibSignature.Signature[] calldata signatures,
uint128[] calldata takerTokenFillAmounts,
bool revertIfIncomplete
)
external
returns (
uint128[] memory takerTokenFilledAmounts,
uint128[] memory makerTokenFilledAmounts
);
}

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../vendor/ILiquidityProvider.sol";
import "../../vendor/ILiquidityProvider.sol";
/// @dev Feature to swap directly with an on-chain liquidity provider.

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./libs/LibSignature.sol";
import "../libs/LibSignature.sol";
/// @dev Meta-transactions feature.
interface IMetaTransactionsFeature {

View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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";
interface IMultiplexFeature {
// Parameters for `batchFill`.
struct BatchFillData {
// The token being sold.
IERC20TokenV06 inputToken;
// The token being bought.
IERC20TokenV06 outputToken;
// The amount of `inputToken` to sell.
uint256 sellAmount;
// The nested calls to perform.
WrappedBatchCall[] calls;
}
// Represents a call nested within a `batchFill`.
struct WrappedBatchCall {
// The selector of the function to call.
bytes4 selector;
// Amount of `inputToken` to sell.
uint256 sellAmount;
// ABI-encoded parameters needed to perform the call.
bytes data;
}
// Parameters for `multiHopFill`.
struct MultiHopFillData {
// The sell path, i.e.
// tokens = [inputToken, hopToken1, ..., hopTokenN, outputToken]
address[] tokens;
// The amount of `tokens[0]` to sell.
uint256 sellAmount;
// The nested calls to perform.
WrappedMultiHopCall[] calls;
}
// Represents a call nested within a `multiHopFill`.
struct WrappedMultiHopCall {
// The selector of the function to call.
bytes4 selector;
// ABI-encoded parameters needed to perform the call.
bytes data;
}
event LiquidityProviderSwap(
address inputToken,
address outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount,
address provider,
address recipient
);
event ExpiredRfqOrder(
bytes32 orderHash,
address maker,
uint64 expiry
);
/// @dev Executes a batch of fills selling `fillData.inputToken`
/// for `fillData.outputToken` in sequence. Refer to the
/// internal variant `_batchFill` for the allowed nested
/// operations.
/// @param fillData Encodes the input/output tokens, the sell
/// amount, and the nested operations for this batch fill.
/// @param minBuyAmount The minimum amount of `fillData.outputToken`
/// to buy. Reverts if this amount is not met.
/// @return outputTokenAmount The amount of the output token bought.
function batchFill(
BatchFillData calldata fillData,
uint256 minBuyAmount
)
external
payable
returns (uint256 outputTokenAmount);
/// @dev Executes a sequence of fills "hopping" through the
/// path of tokens given by `fillData.tokens`. Refer to the
/// internal variant `_multiHopFill` for the allowed nested
/// operations.
/// @param fillData Encodes the path of tokens, the sell amount,
/// and the nested operations for this multi-hop fill.
/// @param minBuyAmount The minimum amount of the output token
/// to buy. Reverts if this amount is not met.
/// @return outputTokenAmount The amount of the output token bought.
function multiHopFill(
MultiHopFillData calldata fillData,
uint256 minBuyAmount
)
external
payable
returns (uint256 outputTokenAmount);
}

View File

@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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 "../libs/LibSignature.sol";
import "../libs/LibNativeOrder.sol";
/// @dev Events emitted by NativeOrdersFeature.
interface INativeOrdersEvents {
/// @dev Emitted whenever a `LimitOrder` is filled.
/// @param orderHash The canonical hash of the order.
/// @param maker The maker of the order.
/// @param taker The taker of the order.
/// @param feeRecipient Fee recipient of the order.
/// @param takerTokenFilledAmount How much taker token was filled.
/// @param makerTokenFilledAmount How much maker token was filled.
/// @param protocolFeePaid How much protocol fee was paid.
/// @param pool The fee pool associated with this order.
event LimitOrderFilled(
bytes32 orderHash,
address maker,
address taker,
address feeRecipient,
address makerToken,
address takerToken,
uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount,
uint128 takerTokenFeeFilledAmount,
uint256 protocolFeePaid,
bytes32 pool
);
/// @dev Emitted whenever an `RfqOrder` is filled.
/// @param orderHash The canonical hash of the order.
/// @param maker The maker of the order.
/// @param taker The taker of the order.
/// @param takerTokenFilledAmount How much taker token was filled.
/// @param makerTokenFilledAmount How much maker token was filled.
/// @param pool The fee pool associated with this order.
event RfqOrderFilled(
bytes32 orderHash,
address maker,
address taker,
address makerToken,
address takerToken,
uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount,
bytes32 pool
);
/// @dev Emitted whenever a limit or RFQ order is cancelled.
/// @param orderHash The canonical hash of the order.
/// @param maker The order maker.
event OrderCancelled(
bytes32 orderHash,
address maker
);
/// @dev Emitted whenever Limit orders are cancelled by pair by a maker.
/// @param maker The maker of the order.
/// @param makerToken The maker token in a pair for the orders cancelled.
/// @param takerToken The taker token in a pair for the orders cancelled.
/// @param minValidSalt The new minimum valid salt an order with this pair must
/// have.
event PairCancelledLimitOrders(
address maker,
address makerToken,
address takerToken,
uint256 minValidSalt
);
/// @dev Emitted whenever RFQ orders are cancelled by pair by a maker.
/// @param maker The maker of the order.
/// @param makerToken The maker token in a pair for the orders cancelled.
/// @param takerToken The taker token in a pair for the orders cancelled.
/// @param minValidSalt The new minimum valid salt an order with this pair must
/// have.
event PairCancelledRfqOrders(
address maker,
address makerToken,
address takerToken,
uint256 minValidSalt
);
/// @dev Emitted when new addresses are allowed or disallowed to fill
/// orders with a given txOrigin.
/// @param origin The address doing the allowing.
/// @param addrs The address being allowed/disallowed.
/// @param allowed Indicates whether the address should be allowed.
event RfqOrderOriginsAllowed(
address origin,
address[] addrs,
bool allowed
);
}

View File

@ -21,98 +21,15 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./libs/LibSignature.sol";
import "./libs/LibNativeOrder.sol";
import "../libs/LibSignature.sol";
import "../libs/LibNativeOrder.sol";
import "./INativeOrdersEvents.sol";
/// @dev Feature for interacting with limit orders.
interface INativeOrdersFeature {
/// @dev Emitted whenever a `LimitOrder` is filled.
/// @param orderHash The canonical hash of the order.
/// @param maker The maker of the order.
/// @param taker The taker of the order.
/// @param feeRecipient Fee recipient of the order.
/// @param takerTokenFilledAmount How much taker token was filled.
/// @param makerTokenFilledAmount How much maker token was filled.
/// @param protocolFeePaid How much protocol fee was paid.
/// @param pool The fee pool associated with this order.
event LimitOrderFilled(
bytes32 orderHash,
address maker,
address taker,
address feeRecipient,
address makerToken,
address takerToken,
uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount,
uint128 takerTokenFeeFilledAmount,
uint256 protocolFeePaid,
bytes32 pool
);
/// @dev Emitted whenever an `RfqOrder` is filled.
/// @param orderHash The canonical hash of the order.
/// @param maker The maker of the order.
/// @param taker The taker of the order.
/// @param takerTokenFilledAmount How much taker token was filled.
/// @param makerTokenFilledAmount How much maker token was filled.
/// @param pool The fee pool associated with this order.
event RfqOrderFilled(
bytes32 orderHash,
address maker,
address taker,
address makerToken,
address takerToken,
uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount,
bytes32 pool
);
/// @dev Emitted whenever a limit or RFQ order is cancelled.
/// @param orderHash The canonical hash of the order.
/// @param maker The order maker.
event OrderCancelled(
bytes32 orderHash,
address maker
);
/// @dev Emitted whenever Limit orders are cancelled by pair by a maker.
/// @param maker The maker of the order.
/// @param makerToken The maker token in a pair for the orders cancelled.
/// @param takerToken The taker token in a pair for the orders cancelled.
/// @param minValidSalt The new minimum valid salt an order with this pair must
/// have.
event PairCancelledLimitOrders(
address maker,
address makerToken,
address takerToken,
uint256 minValidSalt
);
/// @dev Emitted whenever RFQ orders are cancelled by pair by a maker.
/// @param maker The maker of the order.
/// @param makerToken The maker token in a pair for the orders cancelled.
/// @param takerToken The taker token in a pair for the orders cancelled.
/// @param minValidSalt The new minimum valid salt an order with this pair must
/// have.
event PairCancelledRfqOrders(
address maker,
address makerToken,
address takerToken,
uint256 minValidSalt
);
/// @dev Emitted when new addresses are allowed or disallowed to fill
/// orders with a given txOrigin.
/// @param origin The address doing the allowing.
/// @param addrs The address being allowed/disallowed.
/// @param allowed Indicates whether the address should be allowed.
event RfqOrderOriginsAllowed(
address origin,
address[] addrs,
bool allowed
);
interface INativeOrdersFeature is
INativeOrdersEvents
{
/// @dev Transfers protocol fees from the `FeeCollector` pools into
/// the staking contract.

View File

@ -21,8 +21,8 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../transformers/IERC20Transformer.sol";
import "../external/IFlashWallet.sol";
import "../../transformers/IERC20Transformer.sol";
import "../../external/IFlashWallet.sol";
/// @dev Feature to composably transform between ERC20 tokens.

View File

@ -21,10 +21,15 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../../errors/LibNativeOrdersRichErrors.sol";
/// @dev A library for common native order operations.
library LibNativeOrder {
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
enum OrderStatus {
INVALID,
@ -216,4 +221,23 @@ library LibNativeOrder {
structHash := keccak256(mem, 0x160)
}
}
/// @dev Refund any leftover protocol fees in `msg.value` to `msg.sender`.
/// @param ethProtocolFeePaid How much ETH was paid in protocol fees.
function refundExcessProtocolFeeToSender(uint256 ethProtocolFeePaid)
internal
{
if (msg.value > ethProtocolFeePaid && msg.sender != address(this)) {
uint256 refundAmount = msg.value.safeSub(ethProtocolFeePaid);
(bool success,) = msg
.sender
.call{value: refundAmount}("");
if (!success) {
LibNativeOrdersRichErrors.ProtocolFeeRefundFailed(
msg.sender,
refundAmount
).rrevert();
}
}
}
}

View File

@ -0,0 +1,266 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../../errors/LibNativeOrdersRichErrors.sol";
import "../../storage/LibNativeOrdersStorage.sol";
import "../interfaces/INativeOrdersEvents.sol";
import "../libs/LibSignature.sol";
import "../libs/LibNativeOrder.sol";
import "./NativeOrdersInfo.sol";
/// @dev Feature for cancelling limit and RFQ orders.
abstract contract NativeOrdersCancellation is
INativeOrdersEvents,
NativeOrdersInfo
{
using LibRichErrorsV06 for bytes;
/// @dev Highest bit of a uint256, used to flag cancelled orders.
uint256 private constant HIGH_BIT = 1 << 255;
constructor(
address zeroExAddress,
bytes32 greedyTokensBloomFilter
)
internal
NativeOrdersInfo(zeroExAddress, greedyTokensBloomFilter)
{
// solhint-disable no-empty-blocks
}
/// @dev Cancel a single limit order. The caller must be the maker.
/// Silently succeeds if the order has already been cancelled.
/// @param order The limit order.
function cancelLimitOrder(LibNativeOrder.LimitOrder memory order)
public
{
bytes32 orderHash = getLimitOrderHash(order);
if (msg.sender != order.maker) {
LibNativeOrdersRichErrors.OnlyOrderMakerAllowed(
orderHash,
msg.sender,
order.maker
).rrevert();
}
_cancelOrderHash(orderHash, order.maker);
}
/// @dev Cancel a single RFQ order. The caller must be the maker.
/// Silently succeeds if the order has already been cancelled.
/// @param order The RFQ order.
function cancelRfqOrder(LibNativeOrder.RfqOrder memory order)
public
{
bytes32 orderHash = getRfqOrderHash(order);
if (msg.sender != order.maker) {
LibNativeOrdersRichErrors.OnlyOrderMakerAllowed(
orderHash,
msg.sender,
order.maker
).rrevert();
}
_cancelOrderHash(orderHash, order.maker);
}
/// @dev Cancel multiple limit orders. The caller must be the maker.
/// Silently succeeds if the order has already been cancelled.
/// @param orders The limit orders.
function batchCancelLimitOrders(LibNativeOrder.LimitOrder[] memory orders)
public
{
for (uint256 i = 0; i < orders.length; ++i) {
cancelLimitOrder(orders[i]);
}
}
/// @dev Cancel multiple RFQ orders. The caller must be the maker.
/// Silently succeeds if the order has already been cancelled.
/// @param orders The RFQ orders.
function batchCancelRfqOrders(LibNativeOrder.RfqOrder[] memory orders)
public
{
for (uint256 i = 0; i < orders.length; ++i) {
cancelRfqOrder(orders[i]);
}
}
/// @dev Cancel all limit orders for a given maker and pair with a salt less
/// than the value provided. The caller must be the maker. Subsequent
/// calls to this function with the same caller and pair require the
/// new salt to be >= the old salt.
/// @param makerToken The maker token.
/// @param takerToken The taker token.
/// @param minValidSalt The new minimum valid salt.
function cancelPairLimitOrders(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
uint256 minValidSalt
)
public
{
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
uint256 oldMinValidSalt =
stor.limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[msg.sender]
[address(makerToken)]
[address(takerToken)];
// New min salt must >= the old one.
if (oldMinValidSalt > minValidSalt) {
LibNativeOrdersRichErrors.
CancelSaltTooLowError(minValidSalt, oldMinValidSalt)
.rrevert();
}
stor.limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[msg.sender]
[address(makerToken)]
[address(takerToken)] = minValidSalt;
emit PairCancelledLimitOrders(
msg.sender,
address(makerToken),
address(takerToken),
minValidSalt
);
}
/// @dev Cancel all limit orders for a given maker and pair with a salt less
/// than the value provided. The caller must be the maker. Subsequent
/// calls to this function with the same caller and pair require the
/// new salt to be >= the old salt.
/// @param makerTokens The maker tokens.
/// @param takerTokens The taker tokens.
/// @param minValidSalts The new minimum valid salts.
function batchCancelPairLimitOrders(
IERC20TokenV06[] memory makerTokens,
IERC20TokenV06[] memory takerTokens,
uint256[] memory minValidSalts
)
public
{
require(
makerTokens.length == takerTokens.length &&
makerTokens.length == minValidSalts.length,
"NativeOrdersFeature/MISMATCHED_PAIR_ORDERS_ARRAY_LENGTHS"
);
for (uint256 i = 0; i < makerTokens.length; ++i) {
cancelPairLimitOrders(
makerTokens[i],
takerTokens[i],
minValidSalts[i]
);
}
}
/// @dev Cancel all RFQ orders for a given maker and pair with a salt less
/// than the value provided. The caller must be the maker. Subsequent
/// calls to this function with the same caller and pair require the
/// new salt to be >= the old salt.
/// @param makerToken The maker token.
/// @param takerToken The taker token.
/// @param minValidSalt The new minimum valid salt.
function cancelPairRfqOrders(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
uint256 minValidSalt
)
public
{
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
uint256 oldMinValidSalt =
stor.rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[msg.sender]
[address(makerToken)]
[address(takerToken)];
// New min salt must >= the old one.
if (oldMinValidSalt > minValidSalt) {
LibNativeOrdersRichErrors.
CancelSaltTooLowError(minValidSalt, oldMinValidSalt)
.rrevert();
}
stor.rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[msg.sender]
[address(makerToken)]
[address(takerToken)] = minValidSalt;
emit PairCancelledRfqOrders(
msg.sender,
address(makerToken),
address(takerToken),
minValidSalt
);
}
/// @dev Cancel all RFQ orders for a given maker and pair with a salt less
/// than the value provided. The caller must be the maker. Subsequent
/// calls to this function with the same caller and pair require the
/// new salt to be >= the old salt.
/// @param makerTokens The maker tokens.
/// @param takerTokens The taker tokens.
/// @param minValidSalts The new minimum valid salts.
function batchCancelPairRfqOrders(
IERC20TokenV06[] memory makerTokens,
IERC20TokenV06[] memory takerTokens,
uint256[] memory minValidSalts
)
public
{
require(
makerTokens.length == takerTokens.length &&
makerTokens.length == minValidSalts.length,
"NativeOrdersFeature/MISMATCHED_PAIR_ORDERS_ARRAY_LENGTHS"
);
for (uint256 i = 0; i < makerTokens.length; ++i) {
cancelPairRfqOrders(
makerTokens[i],
takerTokens[i],
minValidSalts[i]
);
}
}
/// @dev Cancel a limit or RFQ order directly by its order hash.
/// @param orderHash The order's order hash.
/// @param maker The order's maker.
function _cancelOrderHash(bytes32 orderHash, address maker)
private
{
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
// Set the high bit on the raw taker token fill amount to indicate
// a cancel. It's OK to cancel twice.
stor.orderHashToTakerTokenFilledAmount[orderHash] |= HIGH_BIT;
emit OrderCancelled(orderHash, maker);
}
}

View File

@ -0,0 +1,394 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../../fixins/FixinEIP712.sol";
import "../../fixins/FixinTokenSpender.sol";
import "../../storage/LibNativeOrdersStorage.sol";
import "../libs/LibSignature.sol";
import "../libs/LibNativeOrder.sol";
/// @dev Feature for getting info about limit and RFQ orders.
abstract contract NativeOrdersInfo is
FixinEIP712,
FixinTokenSpender
{
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
// @dev Params for `_getActualFillableTakerTokenAmount()`.
struct GetActualFillableTakerTokenAmountParams {
address maker;
IERC20TokenV06 makerToken;
uint128 orderMakerAmount;
uint128 orderTakerAmount;
LibNativeOrder.OrderInfo orderInfo;
}
/// @dev Highest bit of a uint256, used to flag cancelled orders.
uint256 private constant HIGH_BIT = 1 << 255;
constructor(
address zeroExAddress,
bytes32 greedyTokensBloomFilter
)
internal
FixinEIP712(zeroExAddress)
FixinTokenSpender(greedyTokensBloomFilter)
{
// solhint-disable no-empty-blocks
}
/// @dev Get the order info for a limit order.
/// @param order The limit order.
/// @return orderInfo Info about the order.
function getLimitOrderInfo(LibNativeOrder.LimitOrder memory order)
public
view
returns (LibNativeOrder.OrderInfo memory orderInfo)
{
// Recover maker and compute order hash.
orderInfo.orderHash = getLimitOrderHash(order);
uint256 minValidSalt = LibNativeOrdersStorage.getStorage()
.limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[order.maker]
[address(order.makerToken)]
[address(order.takerToken)];
_populateCommonOrderInfoFields(
orderInfo,
order.takerAmount,
order.expiry,
order.salt,
minValidSalt
);
}
/// @dev Get the order info for an RFQ order.
/// @param order The RFQ order.
/// @return orderInfo Info about the order.
function getRfqOrderInfo(LibNativeOrder.RfqOrder memory order)
public
view
returns (LibNativeOrder.OrderInfo memory orderInfo)
{
// Recover maker and compute order hash.
orderInfo.orderHash = getRfqOrderHash(order);
uint256 minValidSalt = LibNativeOrdersStorage.getStorage()
.rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt
[order.maker]
[address(order.makerToken)]
[address(order.takerToken)];
_populateCommonOrderInfoFields(
orderInfo,
order.takerAmount,
order.expiry,
order.salt,
minValidSalt
);
// Check for missing txOrigin.
if (order.txOrigin == address(0)) {
orderInfo.status = LibNativeOrder.OrderStatus.INVALID;
}
}
/// @dev Get the canonical hash of a limit order.
/// @param order The limit order.
/// @return orderHash The order hash.
function getLimitOrderHash(LibNativeOrder.LimitOrder memory order)
public
view
returns (bytes32 orderHash)
{
return _getEIP712Hash(
LibNativeOrder.getLimitOrderStructHash(order)
);
}
/// @dev Get the canonical hash of an RFQ order.
/// @param order The RFQ order.
/// @return orderHash The order hash.
function getRfqOrderHash(LibNativeOrder.RfqOrder memory order)
public
view
returns (bytes32 orderHash)
{
return _getEIP712Hash(
LibNativeOrder.getRfqOrderStructHash(order)
);
}
/// @dev Get order info, fillable amount, and signature validity for a limit order.
/// Fillable amount is determined using balances and allowances of the maker.
/// @param order The limit order.
/// @param signature The order signature.
/// @return orderInfo Info about the order.
/// @return actualFillableTakerTokenAmount How much of the order is fillable
/// based on maker funds, in taker tokens.
/// @return isSignatureValid Whether the signature is valid.
function getLimitOrderRelevantState(
LibNativeOrder.LimitOrder memory order,
LibSignature.Signature calldata signature
)
public
view
returns (
LibNativeOrder.OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
)
{
orderInfo = getLimitOrderInfo(order);
actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount(
GetActualFillableTakerTokenAmountParams({
maker: order.maker,
makerToken: order.makerToken,
orderMakerAmount: order.makerAmount,
orderTakerAmount: order.takerAmount,
orderInfo: orderInfo
})
);
isSignatureValid = order.maker ==
LibSignature.getSignerOfHash(orderInfo.orderHash, signature);
}
/// @dev Get order info, fillable amount, and signature validity for an RFQ order.
/// Fillable amount is determined using balances and allowances of the maker.
/// @param order The RFQ order.
/// @param signature The order signature.
/// @return orderInfo Info about the order.
/// @return actualFillableTakerTokenAmount How much of the order is fillable
/// based on maker funds, in taker tokens.
/// @return isSignatureValid Whether the signature is valid.
function getRfqOrderRelevantState(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature
)
public
view
returns (
LibNativeOrder.OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
)
{
orderInfo = getRfqOrderInfo(order);
actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount(
GetActualFillableTakerTokenAmountParams({
maker: order.maker,
makerToken: order.makerToken,
orderMakerAmount: order.makerAmount,
orderTakerAmount: order.takerAmount,
orderInfo: orderInfo
})
);
isSignatureValid = order.maker ==
LibSignature.getSignerOfHash(orderInfo.orderHash, signature);
}
/// @dev Batch version of `getLimitOrderRelevantState()`, without reverting.
/// Orders that would normally cause `getLimitOrderRelevantState()`
/// to revert will have empty results.
/// @param orders The limit orders.
/// @param signatures The order signatures.
/// @return orderInfos Info about the orders.
/// @return actualFillableTakerTokenAmounts How much of each order is fillable
/// based on maker funds, in taker tokens.
/// @return isSignatureValids Whether each signature is valid for the order.
function batchGetLimitOrderRelevantStates(
LibNativeOrder.LimitOrder[] calldata orders,
LibSignature.Signature[] calldata signatures
)
external
view
returns (
LibNativeOrder.OrderInfo[] memory orderInfos,
uint128[] memory actualFillableTakerTokenAmounts,
bool[] memory isSignatureValids
)
{
require(
orders.length == signatures.length,
"NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS"
);
orderInfos = new LibNativeOrder.OrderInfo[](orders.length);
actualFillableTakerTokenAmounts = new uint128[](orders.length);
isSignatureValids = new bool[](orders.length);
for (uint256 i = 0; i < orders.length; ++i) {
try
this.getLimitOrderRelevantState(orders[i], signatures[i])
returns (
LibNativeOrder.OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
)
{
orderInfos[i] = orderInfo;
actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount;
isSignatureValids[i] = isSignatureValid;
}
catch {}
}
}
/// @dev Batch version of `getRfqOrderRelevantState()`, without reverting.
/// Orders that would normally cause `getRfqOrderRelevantState()`
/// to revert will have empty results.
/// @param orders The RFQ orders.
/// @param signatures The order signatures.
/// @return orderInfos Info about the orders.
/// @return actualFillableTakerTokenAmounts How much of each order is fillable
/// based on maker funds, in taker tokens.
/// @return isSignatureValids Whether each signature is valid for the order.
function batchGetRfqOrderRelevantStates(
LibNativeOrder.RfqOrder[] calldata orders,
LibSignature.Signature[] calldata signatures
)
external
view
returns (
LibNativeOrder.OrderInfo[] memory orderInfos,
uint128[] memory actualFillableTakerTokenAmounts,
bool[] memory isSignatureValids
)
{
require(
orders.length == signatures.length,
"NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS"
);
orderInfos = new LibNativeOrder.OrderInfo[](orders.length);
actualFillableTakerTokenAmounts = new uint128[](orders.length);
isSignatureValids = new bool[](orders.length);
for (uint256 i = 0; i < orders.length; ++i) {
try
this.getRfqOrderRelevantState(orders[i], signatures[i])
returns (
LibNativeOrder.OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
)
{
orderInfos[i] = orderInfo;
actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount;
isSignatureValids[i] = isSignatureValid;
}
catch {}
}
}
/// @dev Populate `status` and `takerTokenFilledAmount` fields in
/// `orderInfo`, which use the same code path for both limit and
/// RFQ orders.
/// @param orderInfo `OrderInfo` with `orderHash` and `maker` filled.
/// @param takerAmount The order's taker token amount..
/// @param expiry The order's expiry.
/// @param salt The order's salt.
/// @param salt The minimum valid salt for the maker and pair combination.
function _populateCommonOrderInfoFields(
LibNativeOrder.OrderInfo memory orderInfo,
uint128 takerAmount,
uint64 expiry,
uint256 salt,
uint256 minValidSalt
)
private
view
{
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
// Get the filled and direct cancel state.
{
// The high bit of the raw taker token filled amount will be set
// if the order was cancelled.
uint256 rawTakerTokenFilledAmount =
stor.orderHashToTakerTokenFilledAmount[orderInfo.orderHash];
orderInfo.takerTokenFilledAmount = uint128(rawTakerTokenFilledAmount);
if (orderInfo.takerTokenFilledAmount >= takerAmount) {
orderInfo.status = LibNativeOrder.OrderStatus.FILLED;
return;
}
if (rawTakerTokenFilledAmount & HIGH_BIT != 0) {
orderInfo.status = LibNativeOrder.OrderStatus.CANCELLED;
return;
}
}
// Check for expiration.
if (expiry <= uint64(block.timestamp)) {
orderInfo.status = LibNativeOrder.OrderStatus.EXPIRED;
return;
}
// Check if the order was cancelled by salt.
if (minValidSalt > salt) {
orderInfo.status = LibNativeOrder.OrderStatus.CANCELLED;
return;
}
orderInfo.status = LibNativeOrder.OrderStatus.FILLABLE;
}
/// @dev Calculate the actual fillable taker token amount of an order
/// based on maker allowance and balances.
function _getActualFillableTakerTokenAmount(
GetActualFillableTakerTokenAmountParams memory params
)
private
view
returns (uint128 actualFillableTakerTokenAmount)
{
if (params.orderMakerAmount == 0 || params.orderTakerAmount == 0) {
// Empty order.
return 0;
}
if (params.orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
// Not fillable.
return 0;
}
// Get the fillable maker amount based on the order quantities and
// previously filled amount
uint256 fillableMakerTokenAmount = LibMathV06.getPartialAmountFloor(
uint256(
params.orderTakerAmount
- params.orderInfo.takerTokenFilledAmount
),
uint256(params.orderTakerAmount),
uint256(params.orderMakerAmount)
);
// Clamp it to the amount of maker tokens we can spend on behalf of the
// maker.
fillableMakerTokenAmount = LibSafeMathV06.min256(
fillableMakerTokenAmount,
_getSpendableERC20BalanceOf(params.makerToken, params.maker)
);
// Convert to taker token amount.
return LibMathV06.getPartialAmountCeil(
fillableMakerTokenAmount,
uint256(params.orderMakerAmount),
uint256(params.orderTakerAmount)
).safeDowncastToUint128();
}
}

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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/IEtherTokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../../fixins/FixinProtocolFees.sol";
import "../../errors/LibNativeOrdersRichErrors.sol";
import "../../vendor/v3/IStaking.sol";
/// @dev Mixin for protocol fee utility functions.
abstract contract NativeOrdersProtocolFees is
FixinProtocolFees
{
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
constructor(
IEtherTokenV06 weth,
IStaking staking,
FeeCollectorController feeCollectorController,
uint32 protocolFeeMultiplier
)
internal
FixinProtocolFees(weth, staking, feeCollectorController, protocolFeeMultiplier)
{
// solhint-disable no-empty-blocks
}
/// @dev Transfers protocol fees from the `FeeCollector` pools into
/// the staking contract.
/// @param poolIds Staking pool IDs
function transferProtocolFeesForPools(bytes32[] calldata poolIds)
external
{
for (uint256 i = 0; i < poolIds.length; ++i) {
_transferFeesForPool(poolIds[i]);
}
}
/// @dev Get the protocol fee multiplier. This should be multiplied by the
/// gas price to arrive at the required protocol fee to fill a native order.
/// @return multiplier The protocol fee multiplier.
function getProtocolFeeMultiplier()
external
view
returns (uint32 multiplier)
{
return PROTOCOL_FEE_MULTIPLIER;
}
}

View File

@ -0,0 +1,569 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../../errors/LibNativeOrdersRichErrors.sol";
import "../../fixins/FixinCommon.sol";
import "../../storage/LibNativeOrdersStorage.sol";
import "../../vendor/v3/IStaking.sol";
import "../interfaces/INativeOrdersEvents.sol";
import "../libs/LibSignature.sol";
import "../libs/LibNativeOrder.sol";
import "./NativeOrdersCancellation.sol";
import "./NativeOrdersProtocolFees.sol";
/// @dev Mixin for settling limit and RFQ orders.
abstract contract NativeOrdersSettlement is
INativeOrdersEvents,
NativeOrdersCancellation,
NativeOrdersProtocolFees,
FixinCommon
{
using LibSafeMathV06 for uint128;
using LibRichErrorsV06 for bytes;
/// @dev Params for `_settleOrder()`.
struct SettleOrderInfo {
// Order hash.
bytes32 orderHash;
// Maker of the order.
address maker;
// Taker of the order.
address taker;
// Maker token.
IERC20TokenV06 makerToken;
// Taker token.
IERC20TokenV06 takerToken;
// Maker token amount.
uint128 makerAmount;
// Taker token amount.
uint128 takerAmount;
// Maximum taker token amount to fill.
uint128 takerTokenFillAmount;
// How much taker token amount has already been filled in this order.
uint128 takerTokenFilledAmount;
}
/// @dev Params for `_fillLimitOrderPrivate()`
struct FillLimitOrderPrivateParams {
// The limit order.
LibNativeOrder.LimitOrder order;
// The order signature.
LibSignature.Signature signature;
// Maximum taker token to fill this order with.
uint128 takerTokenFillAmount;
// The order taker.
address taker;
// The order sender.
address sender;
}
// @dev Fill results returned by `_fillLimitOrderPrivate()` and
/// `_fillRfqOrderPrivate()`.
struct FillNativeOrderResults {
uint256 ethProtocolFeePaid;
uint128 takerTokenFilledAmount;
uint128 makerTokenFilledAmount;
uint128 takerTokenFeeFilledAmount;
}
constructor(
address zeroExAddress,
IEtherTokenV06 weth,
IStaking staking,
FeeCollectorController feeCollectorController,
uint32 protocolFeeMultiplier,
bytes32 greedyTokensBloomFilter
)
public
NativeOrdersCancellation(zeroExAddress, greedyTokensBloomFilter)
NativeOrdersProtocolFees(weth, staking, feeCollectorController, protocolFeeMultiplier)
{
// solhint-disable no-empty-blocks
}
/// @dev Fill a limit order. The taker and sender will be the caller.
/// @param order The limit order. ETH protocol fees can be
/// attached to this call. Any unspent ETH will be refunded to
/// the caller.
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token amount to fill this order with.
/// @return takerTokenFilledAmount How much maker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillLimitOrder(
LibNativeOrder.LimitOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount
)
public
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillLimitOrderPrivate(FillLimitOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: msg.sender,
sender: msg.sender
}));
LibNativeOrder.refundExcessProtocolFeeToSender(results.ethProtocolFeePaid);
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
);
}
/// @dev Fill an RFQ order for up to `takerTokenFillAmount` taker tokens.
/// The taker will be the caller. ETH should be attached to pay the
/// protocol fee.
/// @param order The RFQ order.
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token amount to fill this order with.
/// @return takerTokenFilledAmount How much maker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillRfqOrder(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount
)
public
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
order,
signature,
takerTokenFillAmount,
msg.sender
);
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
);
}
/// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens.
/// The taker will be the caller. ETH protocol fees can be
/// attached to this call. Any unspent ETH will be refunded to
/// the caller.
/// @param order The limit order.
/// @param signature The order signature.
/// @param takerTokenFillAmount How much taker token to fill this order with.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOrKillLimitOrder(
LibNativeOrder.LimitOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount
)
public
payable
returns (uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillLimitOrderPrivate(FillLimitOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: msg.sender,
sender: msg.sender
}));
// Must have filled exactly the amount requested.
if (results.takerTokenFilledAmount < takerTokenFillAmount) {
LibNativeOrdersRichErrors.FillOrKillFailedError(
getLimitOrderHash(order),
results.takerTokenFilledAmount,
takerTokenFillAmount
).rrevert();
}
LibNativeOrder.refundExcessProtocolFeeToSender(results.ethProtocolFeePaid);
makerTokenFilledAmount = results.makerTokenFilledAmount;
}
/// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens.
/// The taker will be the caller. ETH protocol fees can be
/// attached to this call. Any unspent ETH will be refunded to
/// the caller.
/// @param order The RFQ order.
/// @param signature The order signature.
/// @param takerTokenFillAmount How much taker token to fill this order with.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOrKillRfqOrder(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount
)
public
returns (uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
order,
signature,
takerTokenFillAmount,
msg.sender
);
// Must have filled exactly the amount requested.
if (results.takerTokenFilledAmount < takerTokenFillAmount) {
LibNativeOrdersRichErrors.FillOrKillFailedError(
getRfqOrderHash(order),
results.takerTokenFilledAmount,
takerTokenFillAmount
).rrevert();
}
makerTokenFilledAmount = results.makerTokenFilledAmount;
}
/// @dev Fill a limit order. Internal variant. ETH protocol fees can be
/// attached to this call.
/// @param order The limit order.
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token to fill this order with.
/// @param taker The order taker.
/// @param sender The order sender.
/// @return takerTokenFilledAmount How much maker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillLimitOrder(
LibNativeOrder.LimitOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount,
address taker,
address sender
)
public
virtual
payable
onlySelf
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillLimitOrderPrivate(FillLimitOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: taker,
sender: sender
}));
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
);
}
/// @dev Fill an RFQ order. Internal variant. ETH protocol fees can be
/// attached to this call. Any unspent ETH will be refunded to
/// `msg.sender` (not `sender`).
/// @param order The RFQ order.
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token to fill this order with.
/// @param taker The order taker.
/// @return takerTokenFilledAmount How much maker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillRfqOrder(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount,
address taker
)
public
virtual
onlySelf
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
order,
signature,
takerTokenFillAmount,
taker
);
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
);
}
/// @dev Mark what tx.origin addresses are allowed to fill an order that
/// specifies the message sender as its txOrigin.
/// @param origins An array of origin addresses to update.
/// @param allowed True to register, false to unregister.
function registerAllowedRfqOrigins(
address[] memory origins,
bool allowed
)
external
{
require(msg.sender == tx.origin,
"NativeOrdersFeature/NO_CONTRACT_ORIGINS");
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
for (uint256 i = 0; i < origins.length; i++) {
stor.originRegistry[msg.sender][origins[i]] = allowed;
}
emit RfqOrderOriginsAllowed(msg.sender, origins, allowed);
}
/// @dev Fill a limit order. Private variant. Does not refund protocol fees.
/// @param params Function params.
/// @return results Results of the fill.
function _fillLimitOrderPrivate(FillLimitOrderPrivateParams memory params)
private
returns (FillNativeOrderResults memory results)
{
LibNativeOrder.OrderInfo memory orderInfo = getLimitOrderInfo(params.order);
// Must be fillable.
if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
LibNativeOrdersRichErrors.OrderNotFillableError(
orderInfo.orderHash,
uint8(orderInfo.status)
).rrevert();
}
// Must be fillable by the taker.
if (params.order.taker != address(0) && params.order.taker != params.taker) {
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderInfo.orderHash,
params.taker,
params.order.taker
).rrevert();
}
// Must be fillable by the sender.
if (params.order.sender != address(0) && params.order.sender != params.sender) {
LibNativeOrdersRichErrors.OrderNotFillableBySenderError(
orderInfo.orderHash,
params.sender,
params.order.sender
).rrevert();
}
// Signature must be valid for the order.
{
address signer = LibSignature.getSignerOfHash(
orderInfo.orderHash,
params.signature
);
if (signer != params.order.maker) {
LibNativeOrdersRichErrors.OrderNotSignedByMakerError(
orderInfo.orderHash,
signer,
params.order.maker
).rrevert();
}
}
// Pay the protocol fee.
results.ethProtocolFeePaid = _collectProtocolFee(params.order.pool);
// Settle between the maker and taker.
(results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder(
SettleOrderInfo({
orderHash: orderInfo.orderHash,
maker: params.order.maker,
taker: params.taker,
makerToken: IERC20TokenV06(params.order.makerToken),
takerToken: IERC20TokenV06(params.order.takerToken),
makerAmount: params.order.makerAmount,
takerAmount: params.order.takerAmount,
takerTokenFillAmount: params.takerTokenFillAmount,
takerTokenFilledAmount: orderInfo.takerTokenFilledAmount
})
);
// Pay the fee recipient.
if (params.order.takerTokenFeeAmount > 0) {
results.takerTokenFeeFilledAmount = uint128(LibMathV06.getPartialAmountFloor(
results.takerTokenFilledAmount,
params.order.takerAmount,
params.order.takerTokenFeeAmount
));
_transferERC20Tokens(
params.order.takerToken,
params.taker,
params.order.feeRecipient,
uint256(results.takerTokenFeeFilledAmount)
);
}
emit LimitOrderFilled(
orderInfo.orderHash,
params.order.maker,
params.taker,
params.order.feeRecipient,
address(params.order.makerToken),
address(params.order.takerToken),
results.takerTokenFilledAmount,
results.makerTokenFilledAmount,
results.takerTokenFeeFilledAmount,
results.ethProtocolFeePaid,
params.order.pool
);
}
/// @dev Fill an RFQ order. Private variant. Does not refund protocol fees.
/// @param order The RFQ order.
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token to fill this order with.
/// @param taker The order taker.
/// @return results Results of the fill.
function _fillRfqOrderPrivate(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount,
address taker
)
private
returns (FillNativeOrderResults memory results)
{
LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(order);
// Must be fillable.
if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
LibNativeOrdersRichErrors.OrderNotFillableError(
orderInfo.orderHash,
uint8(orderInfo.status)
).rrevert();
}
{
LibNativeOrdersStorage.Storage storage stor =
LibNativeOrdersStorage.getStorage();
// Must be fillable by the tx.origin.
if (order.txOrigin != tx.origin && !stor.originRegistry[order.txOrigin][tx.origin]) {
LibNativeOrdersRichErrors.OrderNotFillableByOriginError(
orderInfo.orderHash,
tx.origin,
order.txOrigin
).rrevert();
}
}
// Must be fillable by the taker.
if (order.taker != address(0) && order.taker != taker) {
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderInfo.orderHash,
taker,
order.taker
).rrevert();
}
// Signature must be valid for the order.
{
address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature);
if (signer != order.maker) {
LibNativeOrdersRichErrors.OrderNotSignedByMakerError(
orderInfo.orderHash,
signer,
order.maker
).rrevert();
}
}
// Settle between the maker and taker.
(results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder(
SettleOrderInfo({
orderHash: orderInfo.orderHash,
maker: order.maker,
taker: taker,
makerToken: IERC20TokenV06(order.makerToken),
takerToken: IERC20TokenV06(order.takerToken),
makerAmount: order.makerAmount,
takerAmount: order.takerAmount,
takerTokenFillAmount: takerTokenFillAmount,
takerTokenFilledAmount: orderInfo.takerTokenFilledAmount
})
);
emit RfqOrderFilled(
orderInfo.orderHash,
order.maker,
taker,
address(order.makerToken),
address(order.takerToken),
results.takerTokenFilledAmount,
results.makerTokenFilledAmount,
order.pool
);
}
/// @dev Settle the trade between an order's maker and taker.
/// @param settleInfo Information needed to execute the settlement.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _settleOrder(SettleOrderInfo memory settleInfo)
private
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Clamp the taker token fill amount to the fillable amount.
takerTokenFilledAmount = LibSafeMathV06.min128(
settleInfo.takerTokenFillAmount,
settleInfo.takerAmount.safeSub128(settleInfo.takerTokenFilledAmount)
);
// Compute the maker token amount.
// This should never overflow because the values are all clamped to
// (2^128-1).
makerTokenFilledAmount = uint128(LibMathV06.getPartialAmountFloor(
uint256(takerTokenFilledAmount),
uint256(settleInfo.takerAmount),
uint256(settleInfo.makerAmount)
));
if (takerTokenFilledAmount == 0 || makerTokenFilledAmount == 0) {
// Nothing to do.
return (0, 0);
}
// Update filled state for the order.
LibNativeOrdersStorage
.getStorage()
.orderHashToTakerTokenFilledAmount[settleInfo.orderHash] =
// OK to overwrite the whole word because we shouldn't get to this
// function if the order is cancelled.
settleInfo.takerTokenFilledAmount.safeAdd128(takerTokenFilledAmount);
// Transfer taker -> maker.
_transferERC20Tokens(
settleInfo.takerToken,
settleInfo.taker,
settleInfo.maker,
takerTokenFilledAmount
);
// Transfer maker -> taker.
_transferERC20Tokens(
settleInfo.makerToken,
settleInfo.maker,
settleInfo.taker,
makerTokenFilledAmount
);
}
}

View File

@ -23,8 +23,8 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../errors/LibCommonRichErrors.sol";
import "../errors/LibOwnableRichErrors.sol";
import "../features/IOwnableFeature.sol";
import "../features/ISimpleFunctionRegistryFeature.sol";
import "../features/interfaces/IOwnableFeature.sol";
import "../features/interfaces/ISimpleFunctionRegistryFeature.sol";
/// @dev Common feature utilities.

View File

@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../features/ITokenSpenderFeature.sol";
import "../features/interfaces/ITokenSpenderFeature.sol";
import "../errors/LibSpenderRichErrors.sol";
import "../external/FeeCollector.sol";
import "../vendor/v3/IStaking.sol";

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../ZeroEx.sol";
import "../features/IOwnableFeature.sol";
import "../features/interfaces/IOwnableFeature.sol";
import "../features/TokenSpenderFeature.sol";
import "../features/TransformERC20Feature.sol";
import "../features/MetaTransactionsFeature.sol";

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../ZeroEx.sol";
import "../features/IBootstrapFeature.sol";
import "../features/interfaces/IBootstrapFeature.sol";
import "../features/SimpleFunctionRegistryFeature.sol";
import "../features/OwnableFeature.sol";
import "./LibBootstrap.sol";

View File

@ -26,7 +26,7 @@ import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "../features/INativeOrdersFeature.sol";
import "../features/interfaces/INativeOrdersFeature.sol";
import "../features/libs/LibNativeOrder.sol";
import "./bridges/IBridgeAdapter.sol";
import "./Transformer.sol";
@ -132,8 +132,6 @@ contract FillQuoteTransformer is
/// @param orderHash The hash of the order that was skipped.
event ProtocolFeeUnfunded(bytes32 orderHash);
/// @dev Maximum uint256 value.
uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev The highest bit of a uint256 value.
uint256 private constant HIGH_BIT = 2 ** 255;
/// @dev Mask of the lower 255 bits of a uint256 value.

View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 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.12;
interface IUniswapV2Pair {
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data
) external;
function getReserves()
external
view
returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
}

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/ZeroEx.sol";
import "../src/features/IBootstrapFeature.sol";
import "../src/features/interfaces/IBootstrapFeature.sol";
import "../src/migrations/InitialMigration.sol";

View File

@ -20,8 +20,8 @@
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/features/interfaces/IMetaTransactionsFeature.sol";
import "../src/features/NativeOrdersFeature.sol";
import "../src/features/IMetaTransactionsFeature.sol";
import "./TestFeeCollectorController.sol";

View File

@ -20,8 +20,8 @@
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/features/interfaces/IMetaTransactionsFeature.sol";
import "../src/features/TransformERC20Feature.sol";
import "../src/features/IMetaTransactionsFeature.sol";
contract TestMetaTransactionsTransformERC20Feature is

View File

@ -21,7 +21,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/migrations/LibMigrate.sol";
import "../src/features/IOwnableFeature.sol";
import "../src/features/interfaces/IOwnableFeature.sol";
contract TestMigrator {

View File

@ -20,7 +20,7 @@
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/features/INativeOrdersFeature.sol";
import "../src/features/interfaces/INativeOrdersFeature.sol";
contract TestRfqOriginRegistration {
function registerAllowedRfqOrigins(

View File

@ -41,9 +41,9 @@
"rollback": "node ./lib/scripts/rollback.js"
},
"config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider",
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json"
},
"repository": {
"type": "git",

View File

@ -6,6 +6,7 @@
import { ContractArtifact } from 'ethereum-types';
import * as AffiliateFeeTransformer from '../generated-artifacts/AffiliateFeeTransformer.json';
import * as BatchFillNativeOrdersFeature from '../generated-artifacts/BatchFillNativeOrdersFeature.json';
import * as BridgeAdapter from '../generated-artifacts/BridgeAdapter.json';
import * as CurveLiquidityProvider from '../generated-artifacts/CurveLiquidityProvider.json';
import * as FeeCollector from '../generated-artifacts/FeeCollector.json';
@ -13,9 +14,11 @@ import * as FeeCollectorController from '../generated-artifacts/FeeCollectorCont
import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json';
import * as FullMigration from '../generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json';
import * as IBatchFillNativeOrdersFeature from '../generated-artifacts/IBatchFillNativeOrdersFeature.json';
import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json';
import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json';
import * as ILiquidityProviderFeature from '../generated-artifacts/ILiquidityProviderFeature.json';
import * as IMultiplexFeature from '../generated-artifacts/IMultiplexFeature.json';
import * as INativeOrdersFeature from '../generated-artifacts/INativeOrdersFeature.json';
import * as InitialMigration from '../generated-artifacts/InitialMigration.json';
import * as IOwnableFeature from '../generated-artifacts/IOwnableFeature.json';
@ -26,6 +29,7 @@ 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 MultiplexFeature from '../generated-artifacts/MultiplexFeature.json';
import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json';
import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json';
import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json';
@ -66,4 +70,8 @@ export const artifacts = {
FeeCollectorController: FeeCollectorController as ContractArtifact,
FeeCollector: FeeCollector as ContractArtifact,
CurveLiquidityProvider: CurveLiquidityProvider as ContractArtifact,
BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact,
IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact,
MultiplexFeature: MultiplexFeature as ContractArtifact,
IMultiplexFeature: IMultiplexFeature as ContractArtifact,
};

View File

@ -4,6 +4,7 @@
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/affiliate_fee_transformer';
export * from '../generated-wrappers/batch_fill_native_orders_feature';
export * from '../generated-wrappers/bridge_adapter';
export * from '../generated-wrappers/curve_liquidity_provider';
export * from '../generated-wrappers/fee_collector';
@ -11,9 +12,11 @@ export * from '../generated-wrappers/fee_collector_controller';
export * from '../generated-wrappers/fill_quote_transformer';
export * from '../generated-wrappers/full_migration';
export * from '../generated-wrappers/i_allowance_target';
export * from '../generated-wrappers/i_batch_fill_native_orders_feature';
export * from '../generated-wrappers/i_erc20_transformer';
export * from '../generated-wrappers/i_flash_wallet';
export * from '../generated-wrappers/i_liquidity_provider_feature';
export * from '../generated-wrappers/i_multiplex_feature';
export * from '../generated-wrappers/i_native_orders_feature';
export * from '../generated-wrappers/i_ownable_feature';
export * from '../generated-wrappers/i_simple_function_registry_feature';
@ -24,6 +27,7 @@ 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/multiplex_feature';
export * from '../generated-wrappers/native_orders_feature';
export * from '../generated-wrappers/ownable_feature';
export * from '../generated-wrappers/pay_taker_transformer';

View File

@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types';
import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateFeeTransformer.json';
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
import * as BatchFillNativeOrdersFeature from '../test/generated-artifacts/BatchFillNativeOrdersFeature.json';
import * as BootstrapFeature from '../test/generated-artifacts/BootstrapFeature.json';
import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json';
import * as BridgeSource from '../test/generated-artifacts/BridgeSource.json';
@ -22,6 +23,7 @@ import * as FixinTokenSpender from '../test/generated-artifacts/FixinTokenSpende
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
import * as IBatchFillNativeOrdersFeature from '../test/generated-artifacts/IBatchFillNativeOrdersFeature.json';
import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeature.json';
import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json';
import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json';
@ -33,6 +35,8 @@ import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidi
import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json';
import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json';
import * as IMooniswapPool from '../test/generated-artifacts/IMooniswapPool.json';
import * as IMultiplexFeature from '../test/generated-artifacts/IMultiplexFeature.json';
import * as INativeOrdersEvents from '../test/generated-artifacts/INativeOrdersEvents.json';
import * as INativeOrdersFeature from '../test/generated-artifacts/INativeOrdersFeature.json';
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json';
@ -42,6 +46,7 @@ import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts
import * as ITokenSpenderFeature from '../test/generated-artifacts/ITokenSpenderFeature.json';
import * as ITransformERC20Feature from '../test/generated-artifacts/ITransformERC20Feature.json';
import * as IUniswapFeature from '../test/generated-artifacts/IUniswapFeature.json';
import * as IUniswapV2Pair from '../test/generated-artifacts/IUniswapV2Pair.json';
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';
@ -90,7 +95,12 @@ 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';
import * as MooniswapLiquidityProvider from '../test/generated-artifacts/MooniswapLiquidityProvider.json';
import * as MultiplexFeature from '../test/generated-artifacts/MultiplexFeature.json';
import * as NativeOrdersCancellation from '../test/generated-artifacts/NativeOrdersCancellation.json';
import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFeature.json';
import * as NativeOrdersInfo from '../test/generated-artifacts/NativeOrdersInfo.json';
import * as NativeOrdersProtocolFees from '../test/generated-artifacts/NativeOrdersProtocolFees.json';
import * as NativeOrdersSettlement from '../test/generated-artifacts/NativeOrdersSettlement.json';
import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json';
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json';
@ -167,27 +177,36 @@ export const artifacts = {
LiquidityProviderSandbox: LiquidityProviderSandbox as ContractArtifact,
PermissionlessTransformerDeployer: PermissionlessTransformerDeployer as ContractArtifact,
TransformerDeployer: TransformerDeployer as ContractArtifact,
BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact,
BootstrapFeature: BootstrapFeature as ContractArtifact,
IBootstrapFeature: IBootstrapFeature as ContractArtifact,
IFeature: IFeature as ContractArtifact,
ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact,
IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact,
INativeOrdersFeature: INativeOrdersFeature as ContractArtifact,
IOwnableFeature: IOwnableFeature as ContractArtifact,
ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact,
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature as ContractArtifact,
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
MultiplexFeature: MultiplexFeature as ContractArtifact,
NativeOrdersFeature: NativeOrdersFeature as ContractArtifact,
OwnableFeature: OwnableFeature as ContractArtifact,
SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact,
TokenSpenderFeature: TokenSpenderFeature as ContractArtifact,
TransformERC20Feature: TransformERC20Feature as ContractArtifact,
UniswapFeature: UniswapFeature as ContractArtifact,
IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact,
IBootstrapFeature: IBootstrapFeature as ContractArtifact,
IFeature: IFeature as ContractArtifact,
ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact,
IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact,
IMultiplexFeature: IMultiplexFeature as ContractArtifact,
INativeOrdersEvents: INativeOrdersEvents as ContractArtifact,
INativeOrdersFeature: INativeOrdersFeature as ContractArtifact,
IOwnableFeature: IOwnableFeature as ContractArtifact,
ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact,
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature as ContractArtifact,
LibNativeOrder: LibNativeOrder as ContractArtifact,
LibSignature: LibSignature as ContractArtifact,
NativeOrdersCancellation: NativeOrdersCancellation as ContractArtifact,
NativeOrdersInfo: NativeOrdersInfo as ContractArtifact,
NativeOrdersProtocolFees: NativeOrdersProtocolFees as ContractArtifact,
NativeOrdersSettlement: NativeOrdersSettlement as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact,
FixinEIP712: FixinEIP712 as ContractArtifact,
FixinProtocolFees: FixinProtocolFees as ContractArtifact,
@ -238,6 +257,7 @@ export const artifacts = {
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
IMooniswapPool: IMooniswapPool as ContractArtifact,
IUniswapV2Pair: IUniswapV2Pair as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
IStaking: IStaking as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,

View File

@ -0,0 +1,479 @@
import {
blockchainTests,
constants,
describe,
expect,
getRandomPortion,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { BatchFillNativeOrdersFeatureContract, IZeroExContract, IZeroExEvents } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import {
assertOrderInfoEquals,
computeLimitOrderFilledAmounts,
computeRfqOrderFilledAmounts,
createExpiry,
getRandomLimitOrder,
getRandomRfqOrder,
NativeOrdersTestEnvironment,
} from '../utils/orders';
import { TestMintableERC20TokenContract } from '../wrappers';
blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
const { NULL_ADDRESS, ZERO_AMOUNT } = constants;
let maker: string;
let taker: string;
let zeroEx: IZeroExContract;
let feature: BatchFillNativeOrdersFeatureContract;
let verifyingContract: string;
let makerToken: TestMintableERC20TokenContract;
let takerToken: TestMintableERC20TokenContract;
let testUtils: NativeOrdersTestEnvironment;
before(async () => {
testUtils = await NativeOrdersTestEnvironment.createAsync(env);
maker = testUtils.maker;
taker = testUtils.taker;
zeroEx = testUtils.zeroEx;
makerToken = testUtils.makerToken;
takerToken = testUtils.takerToken;
verifyingContract = zeroEx.address;
const featureImpl = await BatchFillNativeOrdersFeatureContract.deployFrom0xArtifactAsync(
artifacts.BatchFillNativeOrdersFeature,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
);
const [owner] = await env.getAccountAddressesAsync();
await zeroEx
.migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner)
.awaitTransactionSuccessAsync();
feature = new BatchFillNativeOrdersFeatureContract(
zeroEx.address,
env.provider,
{ ...env.txDefaults, gasPrice: testUtils.gasPrice },
abis,
);
});
function getTestLimitOrder(fields: Partial<LimitOrderFields> = {}): LimitOrder {
return getRandomLimitOrder({
maker,
verifyingContract,
chainId: 1337,
takerToken: takerToken.address,
makerToken: makerToken.address,
taker: NULL_ADDRESS,
sender: NULL_ADDRESS,
...fields,
});
}
function getTestRfqOrder(fields: Partial<RfqOrderFields> = {}): RfqOrder {
return getRandomRfqOrder({
maker,
verifyingContract,
chainId: 1337,
takerToken: takerToken.address,
makerToken: makerToken.address,
txOrigin: taker,
...fields,
});
}
describe('batchFillLimitOrders', () => {
async function assertExpectedFinalBalancesAsync(
orders: LimitOrder[],
takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount),
takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT),
receipt?: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const expectedFeeRecipientBalances: { [feeRecipient: string]: BigNumber } = {};
const { makerTokenFilledAmount, takerTokenFilledAmount } = orders
.map((order, i) =>
computeLimitOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]),
)
.reduce(
(previous, current, i) => {
_.update(expectedFeeRecipientBalances, orders[i].feeRecipient, balance =>
(balance || ZERO_AMOUNT).plus(current.takerTokenFeeFilledAmount),
);
return {
makerTokenFilledAmount: previous.makerTokenFilledAmount.plus(
current.makerTokenFilledAmount,
),
takerTokenFilledAmount: previous.takerTokenFilledAmount.plus(
current.takerTokenFilledAmount,
),
};
},
{ makerTokenFilledAmount: ZERO_AMOUNT, takerTokenFilledAmount: ZERO_AMOUNT },
);
const makerBalance = await takerToken.balanceOf(maker).callAsync();
const takerBalance = await makerToken.balanceOf(taker).callAsync();
expect(makerBalance, 'maker token balance').to.bignumber.eq(takerTokenFilledAmount);
expect(takerBalance, 'taker token balance').to.bignumber.eq(makerTokenFilledAmount);
for (const [feeRecipient, expectedFeeRecipientBalance] of Object.entries(expectedFeeRecipientBalances)) {
const feeRecipientBalance = await takerToken.balanceOf(feeRecipient).callAsync();
expect(feeRecipientBalance, `fee recipient balance`).to.bignumber.eq(expectedFeeRecipientBalance);
}
if (receipt) {
const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1);
const protocolFees = testUtils.protocolFee.times(orders.length);
const totalCost = testUtils.gasPrice.times(receipt.gasUsed).plus(protocolFees);
expect(balanceOfTakerBefore.minus(totalCost), 'taker ETH balance').to.bignumber.eq(balanceOfTakerNow);
}
}
it('Fully fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Partially fills multiple orders', async () => {
const orders = [...new Array(3)].map(getTestLimitOrder);
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount));
const tx = await feature
.batchFillLimitOrders(orders, signatures, fillAmounts, false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Fillable,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: fillAmounts[i],
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map((order, i) => testUtils.createLimitOrderFilledEventArgs(order, fillAmounts[i])),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders, fillAmounts);
});
it('Fills multiple orders and refunds excess ETH', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length).plus(420);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Skips over unfillable orders and refunds excess ETH', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
assertOrderInfoEquals(expiredOrderInfo, {
status: OrderStatus.Expired,
orderHash: expiredOrder.getHash(),
takerTokenFilledAmount: ZERO_AMOUNT,
});
filledOrderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: fillableOrders[i].getHash(),
takerTokenFilledAmount: fillableOrders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
fillableOrders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(fillableOrders);
});
it('Fills multiple orders with revertIfIncomplete=true', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('If revertIfIncomplete==true, reverts on an unfillable order', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
expiredOrder.getHash(),
ZERO_AMOUNT,
expiredOrder.takerAmount,
),
);
});
it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const partiallyFilledOrder = getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT });
const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount);
await testUtils.fillLimitOrderAsync(partiallyFilledOrder, { fillAmount: partialFillAmount });
const orders = [partiallyFilledOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
partiallyFilledOrder.getHash(),
partiallyFilledOrder.takerAmount.minus(partialFillAmount),
partiallyFilledOrder.takerAmount,
),
);
});
});
describe('batchFillRfqOrders', () => {
async function assertExpectedFinalBalancesAsync(
orders: RfqOrder[],
takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount),
takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT),
): Promise<void> {
const { makerTokenFilledAmount, takerTokenFilledAmount } = orders
.map((order, i) =>
computeRfqOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]),
)
.reduce((previous, current) => ({
makerTokenFilledAmount: previous.makerTokenFilledAmount.plus(current.makerTokenFilledAmount),
takerTokenFilledAmount: previous.takerTokenFilledAmount.plus(current.takerTokenFilledAmount),
}));
const makerBalance = await takerToken.balanceOf(maker).callAsync();
const takerBalance = await makerToken.balanceOf(taker).callAsync();
expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount);
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
}
it('Fully fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Partially fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount));
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, fillAmounts, false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Fillable,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: fillAmounts[i],
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map((order, i) => testUtils.createRfqOrderFilledEventArgs(order, fillAmounts[i])),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders, fillAmounts);
});
it('Skips over unfillable orders', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
assertOrderInfoEquals(expiredOrderInfo, {
status: OrderStatus.Expired,
orderHash: expiredOrder.getHash(),
takerTokenFilledAmount: ZERO_AMOUNT,
});
filledOrderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: fillableOrders[i].getHash(),
takerTokenFilledAmount: fillableOrders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
fillableOrders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(fillableOrders);
});
it('Fills multiple orders with revertIfIncomplete=true', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('If revertIfIncomplete==true, reverts on an unfillable order', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
expiredOrder.getHash(),
ZERO_AMOUNT,
expiredOrder.takerAmount,
),
);
});
it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const partiallyFilledOrder = getTestRfqOrder();
const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount);
await testUtils.fillRfqOrderAsync(partiallyFilledOrder, partialFillAmount);
const orders = [partiallyFilledOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
partiallyFilledOrder.getHash(),
partiallyFilledOrder.takerAmount.minus(partialFillAmount),
partiallyFilledOrder.takerAmount,
),
);
});
});
});

View File

@ -0,0 +1,764 @@
import {
artifacts as erc20Artifacts,
ERC20TokenContract,
WETH9Contract,
WETH9DepositEventArgs,
WETH9Events,
WETH9WithdrawalEventArgs,
} from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, filterLogsToArguments, toBaseUnitAmount } from '@0x/contracts-test-utils';
import {
BridgeSource,
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
FillQuoteTransformerOrderType,
FillQuoteTransformerSide,
findTransformerNonce,
RfqOrder,
SIGNATURE_ABI,
} from '@0x/protocol-utils';
import { AbiEncoder, BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { getRandomRfqOrder } from '../utils/orders';
import {
BridgeAdapterBridgeFillEventArgs,
BridgeAdapterEvents,
IUniswapV2PairEvents,
IUniswapV2PairSwapEventArgs,
IZeroExContract,
IZeroExEvents,
IZeroExRfqOrderFilledEventArgs,
MultiplexFeatureContract,
MultiplexFeatureEvents,
MultiplexFeatureLiquidityProviderSwapEventArgs,
SimpleFunctionRegistryFeatureContract,
} from '../wrappers';
const HIGH_BIT = new BigNumber(2).pow(255);
function encodeFractionalFillAmount(frac: number): BigNumber {
return HIGH_BIT.plus(new BigNumber(frac).times('1e18').integerValue());
}
const EP_GOVERNOR = '0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e';
const DAI_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
const WETH_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e';
const USDC_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
blockchainTests.configure({
fork: {
unlockedAccounts: [EP_GOVERNOR, DAI_WALLET, WETH_WALLET, USDC_WALLET],
},
});
interface WrappedBatchCall {
selector: string;
sellAmount: BigNumber;
data: string;
}
blockchainTests.fork.skip('Multiplex feature', env => {
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f';
const dai = new ERC20TokenContract(DAI_ADDRESS, env.provider, env.txDefaults);
const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const weth = new WETH9Contract(WETH_ADDRESS, env.provider, env.txDefaults);
const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const usdt = new ERC20TokenContract(USDT_ADDRESS, env.provider, env.txDefaults);
const LON_ADDRESS = '0x0000000000095413afc295d19edeb1ad7b71c952';
const PLP_SANDBOX_ADDRESS = '0x407b4128e9ecad8769b2332312a9f655cb9f5f3a';
const WETH_DAI_PLP_ADDRESS = '0x1db681925786441ba82adefac7bf492089665ca0';
const WETH_USDC_PLP_ADDRESS = '0x8463c03c0c57ff19fa8b431e0d3a34e2df89888e';
const USDC_USDT_PLP_ADDRESS = '0xc340ef96449514cea4dfa11d847a06d7f03d437c';
const GREEDY_TOKENS_BLOOM_FILTER = '0x0000100800000480002c00401000000820000000000000020000001010800001';
const BALANCER_WETH_DAI = '0x8b6e6e7b5b3801fed2cafd4b22b8a16c2f2db21a';
const fqtNonce = findTransformerNonce(
'0xfa6282736af206cb4cfc5cb786d82aecdf1186f9',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
const payTakerNonce = findTransformerNonce(
'0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
let zeroEx: IZeroExContract;
let multiplex: MultiplexFeatureContract;
let rfqMaker: string;
let flashWalletAddress: string;
before(async () => {
const erc20Abis = _.mapValues(erc20Artifacts, v => v.compilerOutput.abi);
[rfqMaker] = await env.getAccountAddressesAsync();
zeroEx = new IZeroExContract('0xdef1c0ded9bec7f1a1670819833240f027b25eff', env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
const registry = new SimpleFunctionRegistryFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
multiplex = new MultiplexFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
const multiplexImpl = await MultiplexFeatureContract.deployFrom0xArtifactAsync(
artifacts.MultiplexFeature,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
WETH_ADDRESS,
PLP_SANDBOX_ADDRESS,
GREEDY_TOKENS_BLOOM_FILTER,
);
await registry
.extend(multiplex.getSelector('batchFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await registry
.extend(multiplex.getSelector('multiHopFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await dai
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.transfer(rfqMaker, toBaseUnitAmount(100))
.awaitTransactionSuccessAsync({ from: WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
});
describe('batchFill', () => {
let rfqDataEncoder: AbiEncoder.DataType;
let uniswapCall: WrappedBatchCall;
let sushiswapCall: WrappedBatchCall;
let plpCall: WrappedBatchCall;
let rfqCall: WrappedBatchCall;
let rfqOrder: RfqOrder;
before(async () => {
rfqDataEncoder = AbiEncoder.create([
{ name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI },
{ name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
]);
rfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
});
rfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1),
data: rfqDataEncoder.encode({
order: rfqOrder,
signature: await rfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
const plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.01),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
sushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.02),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: true }),
};
plpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
sellAmount: toBaseUnitAmount(1.03),
data: plpDataEncoder.encode({
provider: WETH_DAI_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
});
it('MultiplexFeature.batchFill(RFQ, unused Uniswap fallback)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: rfqCall.sellAmount,
calls: [rfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
it('MultiplexFeature.batchFill(expired RFQ, Uniswap fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(expired RFQ, Balancer FQT fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]);
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: WETH_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Balancer,
takerTokenAmount: expiredRfqCall.sellAmount,
makerTokenAmount: expiredRfqCall.sellAmount,
bridgeData: poolEncoder.encode([BALANCER_WETH_DAI]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: expiredRfqCall.sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [WETH_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
const balancerFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount: expiredRfqCall.sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, balancerFqtCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Balancer);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(WETH_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(expiredRfqCall.sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(Sushiswap, PLP, Uniswap, RFQ)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: BigNumber.sum(
sushiswapCall.sellAmount,
plpCall.sellAmount,
uniswapCall.sellAmount,
rfqCall.sellAmount,
),
calls: [sushiswapCall, plpCall, uniswapCall, rfqCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [sushiswapEvent, uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(sushiswapEvent.sender, 'Sushiswap Swap event sender').to.equal(zeroEx.address);
expect(sushiswapEvent.to, 'Sushiswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(sushiswapEvent.amount0In, sushiswapEvent.amount1In),
'Sushiswap Swap event input amount',
).to.bignumber.equal(sushiswapCall.sellAmount);
expect(
BigNumber.max(sushiswapEvent.amount0Out, sushiswapEvent.amount1Out),
'Sushiswap Swap event output amount',
).to.bignumber.gt(0);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
const [plpEvent] = filterLogsToArguments<MultiplexFeatureLiquidityProviderSwapEventArgs>(
tx.logs,
MultiplexFeatureEvents.LiquidityProviderSwap,
);
expect(plpEvent.inputToken, 'LiquidityProviderSwap event inputToken').to.equal(batchFillData.inputToken);
expect(plpEvent.outputToken, 'LiquidityProviderSwap event outputToken').to.equal(batchFillData.outputToken);
expect(plpEvent.inputTokenAmount, 'LiquidityProviderSwap event inputToken').to.bignumber.equal(
plpCall.sellAmount,
);
expect(plpEvent.outputTokenAmount, 'LiquidityProviderSwap event outputTokenAmount').to.bignumber.gt(0);
expect(plpEvent.provider, 'LiquidityProviderSwap event provider address').to.equal(WETH_DAI_PLP_ADDRESS);
expect(plpEvent.recipient, 'LiquidityProviderSwap event recipient address').to.equal(DAI_WALLET);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
});
describe('multiHopFill', () => {
let uniswapDataEncoder: AbiEncoder.DataType;
let plpDataEncoder: AbiEncoder.DataType;
let curveEncoder: AbiEncoder.DataType;
let transformERC20Encoder: AbiEncoder.DataType;
let batchFillEncoder: AbiEncoder.DataType;
let multiHopFillEncoder: AbiEncoder.DataType;
before(async () => {
uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
curveEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
]);
transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
batchFillEncoder = AbiEncoder.create([
{
name: 'calls',
type: 'tuple[]',
components: [
{ name: 'selector', type: 'bytes4' },
{ name: 'sellAmount', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);
multiHopFillEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{
name: 'calls',
type: 'tuple[]',
components: [{ name: 'selector', type: 'bytes4' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
});
it('MultiplexFeature.multiHopFill(DAI Curve> USDC Uni> WETH unwrap> ETH)', async () => {
const sellAmount = toBaseUnitAmount(1000000); // 1M DAI
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: USDC_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Curve,
takerTokenAmount: sellAmount,
makerTokenAmount: sellAmount,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
0, // DAI
1, // USDC
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [USDC_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDC_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
const unwrapEthCall = {
selector: weth.getSelector('withdraw'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [DAI_ADDRESS, USDC_ADDRESS, WETH_ADDRESS, ETH_TOKEN_ADDRESS],
sellAmount,
calls: [curveFqtCall, uniswapCall, unwrapEthCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(zeroEx.address);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(
bridgeFillEvent.outputTokenAmount,
);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [wethWithdrawalEvent] = filterLogsToArguments<WETH9WithdrawalEventArgs>(
tx.logs,
WETH9Events.Withdrawal,
);
expect(wethWithdrawalEvent._owner, 'WETH Withdrawal event _owner').to.equal(zeroEx.address);
expect(wethWithdrawalEvent._value, 'WETH Withdrawal event _value').to.bignumber.equal(uniswapOutputAmount);
});
it('MultiplexFeature.multiHopFill(ETH wrap-> WETH Uni> USDC Curve> DAI)', async () => {
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: USDC_ADDRESS,
buyToken: DAI_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Curve,
takerTokenAmount: constants.MAX_UINT256,
makerTokenAmount: constants.MAX_UINT256,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
1, // USDC
0, // DAI
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: constants.MAX_UINT256,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [DAI_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS], isSushi: false }),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, USDC_ADDRESS, DAI_ADDRESS],
sellAmount,
calls: [wrapEthCall, uniswapCall, curveFqtCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
const [wethDepositEvent] = filterLogsToArguments<WETH9DepositEventArgs>(tx.logs, WETH9Events.Deposit);
expect(wethDepositEvent._owner, 'WETH Deposit event _owner').to.equal(zeroEx.address);
expect(wethDepositEvent._value, 'WETH Deposit event _value').to.bignumber.equal(sellAmount);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(flashWalletAddress);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(sellAmount);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve);
expect(bridgeFillEvent.inputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(uniswapOutputAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it.skip('MultiplexFeature.multiHopFill() complex scenario', async () => {
/*
/PLP> USDC
/ \
/ PLP
/Uni (via USDC)\
/ V
ETH wrap> WETH Uni/Sushi> USDT Sushi> LON
\ ^
Uni /
*/
// Taker has to have approved the EP for the intermediate tokens :/
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
await usdt
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const wethUsdcPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: WETH_USDC_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const usdcUsdtPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: USDC_USDT_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const wethUsdcUsdtMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.25),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS],
calls: [wethUsdcPlpCall, usdcUsdtPlpCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethUsdcUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtSushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: true }),
};
const wethUsdtBatchCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [
wethUsdcUsdtMultiHopCall,
wethUsdcUsdtUniswapCall,
wethUsdtUniswapCall,
wethUsdtSushiswapCall,
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const usdtLonSushiCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDT_ADDRESS, LON_ADDRESS], isSushi: true }),
};
const wethUsdtLonMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.8),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDT_ADDRESS],
calls: [wethUsdtBatchCall, usdtLonSushiCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethLonUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.2),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, LON_ADDRESS], isSushi: false }),
};
const wethLonBatchFillCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [wethUsdtLonMultiHopCall, wethLonUniswapCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, LON_ADDRESS],
sellAmount,
calls: [wrapEthCall, wethLonBatchFillCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
});
});
});

View File

@ -3,25 +3,28 @@ import {
constants,
describe,
expect,
getRandomPortion,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import {
LimitOrder,
LimitOrderFields,
OrderInfo,
OrderStatus,
RevertErrors,
RfqOrder,
RfqOrderFields,
} from '@0x/protocol-utils';
import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import { AnyRevertError, BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { IZeroExContract, IZeroExEvents } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { fullMigrateAsync } from '../utils/migration';
import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders';
import {
assertOrderInfoEquals,
computeLimitOrderFilledAmounts,
computeRfqOrderFilledAmounts,
createExpiry,
getActualFillableTakerTokenAmount,
getFillableMakerTokenAmount,
getRandomLimitOrder,
getRandomRfqOrder,
NativeOrdersTestEnvironment,
} from '../utils/orders';
import { TestMintableERC20TokenContract, TestRfqOriginRegistrationContract } from '../wrappers';
blockchainTests.resets('NativeOrdersFeature', env => {
@ -39,6 +42,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
let takerToken: TestMintableERC20TokenContract;
let wethToken: TestMintableERC20TokenContract;
let testRfqOriginRegistration: TestRfqOriginRegistrationContract;
let testUtils: NativeOrdersTestEnvironment;
before(async () => {
let owner;
@ -78,6 +82,16 @@ blockchainTests.resets('NativeOrdersFeature', env => {
env.txDefaults,
artifacts,
);
testUtils = new NativeOrdersTestEnvironment(
maker,
taker,
makerToken,
takerToken,
zeroEx,
GAS_PRICE,
SINGLE_PROTOCOL_FEE,
env,
);
});
function getTestLimitOrder(fields: Partial<LimitOrderFields> = {}): LimitOrder {
@ -105,27 +119,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
}
async function prepareBalancesForOrderAsync(order: LimitOrder | RfqOrder, _taker: string = taker): Promise<void> {
await makerToken.mint(maker, order.makerAmount).awaitTransactionSuccessAsync();
if ('takerTokenFeeAmount' in order) {
await takerToken
.mint(_taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
} else {
await takerToken.mint(_taker, order.takerAmount).awaitTransactionSuccessAsync();
}
}
function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void {
expect(actual.status).to.eq(expected.status);
expect(actual.orderHash).to.eq(expected.orderHash);
expect(actual.takerTokenFilledAmount).to.bignumber.eq(expected.takerTokenFilledAmount);
}
function createExpiry(deltaSeconds: number = 60): BigNumber {
return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds);
}
describe('getProtocolFeeMultiplier()', () => {
it('returns the protocol fee multiplier', async () => {
const r = await zeroEx.getProtocolFeeMultiplier().callAsync();
@ -149,26 +142,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
async function fillLimitOrderAsync(
order: LimitOrder,
opts: Partial<{
fillAmount: BigNumber | number;
taker: string;
protocolFee?: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker: _taker, protocolFee } = {
taker,
fillAmount: order.takerAmount,
...opts,
};
await prepareBalancesForOrderAsync(order, _taker);
const _protocolFee = protocolFee === undefined ? SINGLE_PROTOCOL_FEE : protocolFee;
return zeroEx
.fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker, value: _protocolFee });
}
describe('getLimitOrderInfo()', () => {
it('unfilled order', async () => {
const order = getTestLimitOrder();
@ -205,7 +178,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const expiry = createExpiry(60);
const order = getTestLimitOrder({ expiry });
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
// Advance time to expire the order.
await env.web3Wrapper.increaseTimeAsync(61);
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
@ -219,7 +192,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled order', async () => {
const order = getTestLimitOrder();
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Filled,
@ -232,7 +205,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillLimitOrderAsync(order, { fillAmount });
await testUtils.fillLimitOrderAsync(order, { fillAmount });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Fillable,
@ -244,7 +217,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then cancelled order', async () => {
const order = getTestLimitOrder();
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@ -258,7 +231,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillLimitOrderAsync(order, { fillAmount });
await testUtils.fillLimitOrderAsync(order, { fillAmount });
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@ -269,17 +242,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
async function fillRfqOrderAsync(
order: RfqOrder,
fillAmount: BigNumber | number = order.takerAmount,
_taker: string = taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await prepareBalancesForOrderAsync(order, _taker);
return zeroEx
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker });
}
describe('getRfqOrderInfo()', () => {
it('unfilled order', async () => {
const order = getTestRfqOrder();
@ -316,7 +278,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then expired order', async () => {
const expiry = createExpiry(60);
const order = getTestRfqOrder({ expiry });
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const sig = await order.getSignatureWithProviderAsync(env.provider);
// Fill the order first.
await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
@ -333,7 +295,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled order', async () => {
const order = getTestRfqOrder();
// Fill the order first.
await fillRfqOrderAsync(order, order.takerAmount, taker);
await testUtils.fillRfqOrderAsync(order, order.takerAmount, taker);
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Filled,
@ -346,7 +308,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillRfqOrderAsync(order, fillAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Fillable,
@ -358,7 +320,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then cancelled order', async () => {
const order = getTestRfqOrder();
// Fill the order first.
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@ -372,7 +334,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillRfqOrderAsync(order, fillAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@ -408,7 +370,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a fully filled order', async () => {
const order = getTestLimitOrder();
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@ -421,7 +383,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a partially filled order', async () => {
const order = getTestLimitOrder();
await fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
await testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@ -482,7 +444,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a fully filled order', async () => {
const order = getTestRfqOrder();
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@ -495,7 +457,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a partially filled order', async () => {
const order = getTestRfqOrder();
await fillRfqOrderAsync(order, order.takerAmount.minus(1));
await testUtils.fillRfqOrderAsync(order, order.takerAmount.minus(1));
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@ -747,63 +709,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
interface LimitOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
takerTokenFeeFilledAmount: BigNumber;
}
function computeLimitOrderFilledAmounts(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): LimitOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
const takerTokenFeeFilledAmount = fillAmount
.times(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
takerTokenFeeFilledAmount,
};
}
function createLimitOrderFilledEventArgs(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): object {
const {
makerTokenFilledAmount,
takerTokenFilledAmount,
takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO_AMOUNT : SINGLE_PROTOCOL_FEE;
return {
taker,
takerTokenFilledAmount,
makerTokenFilledAmount,
takerTokenFeeFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
feeRecipient: order.feeRecipient,
makerToken: order.makerToken,
takerToken: order.takerToken,
protocolFeePaid: protocolFee,
pool: order.pool,
};
}
async function assertExpectedFinalBalancesFromLimitOrderFillAsync(
order: LimitOrder,
opts: Partial<{
@ -841,10 +746,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillLimitOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestLimitOrder();
const receipt = await fillLimitOrderAsync(order);
const receipt = await testUtils.fillLimitOrderAsync(order);
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@ -858,10 +763,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can partially fill an order', async () => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
const receipt = await fillLimitOrderAsync(order, { fillAmount });
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@ -869,24 +774,26 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Fillable,
takerTokenFilledAmount: fillAmount,
});
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount });
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, {
takerTokenFillAmount: fillAmount,
});
});
it('can fully fill an order in two steps', async () => {
const order = getTestLimitOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillLimitOrderAsync(order, { fillAmount });
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount);
receipt = await fillLimitOrderAsync(order, { fillAmount });
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@ -899,10 +806,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available', async () => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.plus(1);
const receipt = await fillLimitOrderAsync(order, { fillAmount });
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@ -910,24 +817,26 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Filled,
takerTokenFilledAmount: order.takerAmount,
});
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount });
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, {
takerTokenFillAmount: fillAmount,
});
});
it('clamps fill amount to remaining available in partial filled order', async () => {
const order = getTestLimitOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillLimitOrderAsync(order, { fillAmount });
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
receipt = await fillLimitOrderAsync(order, { fillAmount });
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@ -939,7 +848,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an expired order', async () => {
const order = getTestLimitOrder({ expiry: createExpiry(-60) });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
);
@ -948,7 +857,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill a cancelled order', async () => {
const order = getTestLimitOrder();
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@ -959,7 +868,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await zeroEx
.cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1))
.awaitTransactionSuccessAsync({ from: maker });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@ -967,7 +876,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-taker cannot fill order', async () => {
const order = getTestLimitOrder({ taker });
const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
);
@ -975,7 +884,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-sender cannot fill order', async () => {
const order = getTestLimitOrder({ sender: taker });
const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender),
);
@ -985,7 +894,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = fillLimitOrderAsync(order.clone({ chainId: 1234 }));
const tx = testUtils.fillLimitOrderAsync(order.clone({ chainId: 1234 }));
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
);
@ -993,7 +902,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if no protocol fee attached', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const tx = zeroEx
.fillLimitOrder(
order,
@ -1008,62 +917,24 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
const receipt = await fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
const receipt = await testUtils.fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
});
});
interface RfqOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
}
function computeRfqOrderFilledAmounts(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): RfqOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
function createRfqOrderFilledEventArgs(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): object {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
order,
takerTokenFillAmount,
takerTokenAlreadyFilledAmount,
);
return {
taker,
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
makerToken: order.makerToken,
takerToken: order.takerToken,
pool: order.pool,
};
}
describe('registerAllowedRfqOrigins()', () => {
it('cannot register through a contract', async () => {
const tx = testRfqOriginRegistration
.registerAllowedRfqOrigins(zeroEx.address, [], true)
.awaitTransactionSuccessAsync();
expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS');
});
});
async function assertExpectedFinalBalancesFromRfqOrderFillAsync(
order: RfqOrder,
@ -1081,20 +952,15 @@ blockchainTests.resets('NativeOrdersFeature', env => {
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
}
describe('registerAllowedRfqOrigins()', () => {
it('cannot register through a contract', async () => {
const tx = testRfqOriginRegistration
.registerAllowedRfqOrigins(zeroEx.address, [], true)
.awaitTransactionSuccessAsync();
expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS');
});
});
describe('fillRfqOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestRfqOrder();
const receipt = await fillRfqOrderAsync(order);
verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled);
const receipt = await testUtils.fillRfqOrderAsync(order);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createRfqOrderFilledEventArgs(order)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
orderHash: order.getHash(),
status: OrderStatus.Filled,
@ -1106,10 +972,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can partially fill an order', async () => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
const receipt = await fillRfqOrderAsync(order, fillAmount);
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@ -1123,18 +989,18 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can fully fill an order in two steps', async () => {
const order = getTestRfqOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillRfqOrderAsync(order, fillAmount);
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount);
receipt = await fillRfqOrderAsync(order, fillAmount);
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@ -1147,10 +1013,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available', async () => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.plus(1);
const receipt = await fillRfqOrderAsync(order, fillAmount);
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@ -1164,18 +1030,18 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available in partial filled order', async () => {
const order = getTestRfqOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillRfqOrderAsync(order, fillAmount);
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
receipt = await fillRfqOrderAsync(order, fillAmount);
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@ -1187,7 +1053,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an order with wrong tx.origin', async () => {
const order = getTestRfqOrder();
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
);
@ -1210,7 +1076,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
],
IZeroExEvents.RfqOrderOriginsAllowed,
);
return fillRfqOrderAsync(order, order.takerAmount, notTaker);
return testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
});
it('cannot fill an order with registered then unregistered tx.origin', async () => {
@ -1232,7 +1098,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
IZeroExEvents.RfqOrderOriginsAllowed,
);
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
);
@ -1240,7 +1106,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an order with a zero tx.origin', async () => {
const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS });
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid),
);
@ -1248,7 +1114,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-taker cannot fill order', async () => {
const order = getTestRfqOrder({ taker, txOrigin: notTaker });
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
);
@ -1256,7 +1122,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an expired order', async () => {
const order = getTestRfqOrder({ expiry: createExpiry(-60) });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
);
@ -1265,7 +1131,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill a cancelled order', async () => {
const order = getTestRfqOrder();
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@ -1276,7 +1142,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await zeroEx
.cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1))
.awaitTransactionSuccessAsync({ from: maker });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@ -1286,7 +1152,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = fillRfqOrderAsync(order.clone({ chainId: 1234 }));
const tx = testUtils.fillRfqOrderAsync(order.clone({ chainId: 1234 }));
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
);
@ -1294,7 +1160,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if ETH is attached', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order, taker);
await testUtils.prepareBalancesForOrdersAsync([order], taker);
const tx = zeroEx
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
@ -1306,20 +1172,20 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillOrKillLimitOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const receipt = await zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
});
it('reverts if cannot fill the exact amount', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const fillAmount = order.takerAmount.plus(1);
const tx = zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
@ -1331,7 +1197,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const receipt = await zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
@ -1345,16 +1211,20 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillOrKillRfqOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const receipt = await zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createRfqOrderFilledEventArgs(order)],
IZeroExEvents.RfqOrderFilled,
);
});
it('reverts if cannot fill the exact amount', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const fillAmount = order.takerAmount.plus(1);
const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
@ -1366,7 +1236,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if ETH is attached', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
@ -1385,34 +1255,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await makerToken.approve(zeroEx.address, allowance).awaitTransactionSuccessAsync({ from: maker });
}
function getFillableMakerTokenAmount(
order: LimitOrder | RfqOrder,
takerTokenFilledAmount: BigNumber = ZERO_AMOUNT,
): BigNumber {
return order.takerAmount
.minus(takerTokenFilledAmount)
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
}
function getActualFillableTakerTokenAmount(
order: LimitOrder | RfqOrder,
makerBalance: BigNumber = order.makerAmount,
makerAllowance: BigNumber = order.makerAmount,
takerTokenFilledAmount: BigNumber = ZERO_AMOUNT,
): BigNumber {
const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount);
return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance)
.times(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_UP);
}
function getRandomFraction(precision: number = 2): string {
return Math.random().toPrecision(precision);
}
describe('getLimitOrderRelevantState()', () => {
it('works with an empty order', async () => {
const order = getTestLimitOrder({
@ -1487,7 +1329,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await takerToken
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
// Partially fill the order.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
@ -1509,12 +1351,12 @@ blockchainTests.resets('NativeOrdersFeature', env => {
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
// Partially fill the order.
const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue();
await fillLimitOrderAsync(order, { fillAmount });
const fillAmount = getRandomPortion(order.takerAmount);
await testUtils.fillLimitOrderAsync(order, { fillAmount });
// Reduce maker funds to be < remaining.
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
const balance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const balance = getRandomPortion(remainingMakerAmount);
const allowance = getRandomPortion(remainingMakerAmount);
await fundOrderMakerAsync(order, balance, allowance);
// Get order state.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
@ -1604,7 +1446,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
// Fully Fund maker and taker.
await fundOrderMakerAsync(order);
await takerToken.mint(taker, order.takerAmount);
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
// Partially fill the order.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
@ -1624,12 +1466,12 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await fundOrderMakerAsync(order);
await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync();
// Partially fill the order.
const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue();
await fillRfqOrderAsync(order, fillAmount);
const fillAmount = getRandomPortion(order.takerAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
// Reduce maker funds to be < remaining.
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
const balance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const balance = getRandomPortion(remainingMakerAmount);
const allowance = getRandomPortion(remainingMakerAmount);
await fundOrderMakerAsync(order, balance, allowance);
// Get order state.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx

View File

@ -1,6 +1,181 @@
import { getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import {
BlockchainTestsEnvironment,
constants,
expect,
getRandomInteger,
randomAddress,
} from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, OrderBase, OrderInfo, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { IZeroExContract, IZeroExLimitOrderFilledEventArgs, IZeroExRfqOrderFilledEventArgs } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { fullMigrateAsync } from '../utils/migration';
import { TestMintableERC20TokenContract } from '../wrappers';
const { ZERO_AMOUNT: ZERO, NULL_ADDRESS } = constants;
interface RfqOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
}
interface LimitOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
takerTokenFeeFilledAmount: BigNumber;
}
export class NativeOrdersTestEnvironment {
public static async createAsync(
env: BlockchainTestsEnvironment,
gasPrice: BigNumber = new BigNumber('123e9'),
protocolFeeMultiplier: number = 70e3,
): Promise<NativeOrdersTestEnvironment> {
const [owner, maker, taker] = await env.getAccountAddressesAsync();
const [makerToken, takerToken] = await Promise.all(
[...new Array(2)].map(async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
{ ...env.txDefaults, gasPrice },
artifacts,
),
),
);
const zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}, { protocolFeeMultiplier });
await makerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
await takerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
return new NativeOrdersTestEnvironment(
maker,
taker,
makerToken,
takerToken,
zeroEx,
gasPrice,
gasPrice.times(protocolFeeMultiplier),
env,
);
}
constructor(
public readonly maker: string,
public readonly taker: string,
public readonly makerToken: TestMintableERC20TokenContract,
public readonly takerToken: TestMintableERC20TokenContract,
public readonly zeroEx: IZeroExContract,
public readonly gasPrice: BigNumber,
public readonly protocolFee: BigNumber,
private readonly _env: BlockchainTestsEnvironment,
) {}
public async prepareBalancesForOrdersAsync(
orders: LimitOrder[] | RfqOrder[],
taker: string = this.taker,
): Promise<void> {
await this.makerToken
.mint(this.maker, BigNumber.sum(...(orders as OrderBase[]).map(order => order.makerAmount)))
.awaitTransactionSuccessAsync();
await this.takerToken
.mint(
taker,
BigNumber.sum(
...(orders as OrderBase[]).map(order =>
order.takerAmount.plus(order instanceof LimitOrder ? order.takerTokenFeeAmount : 0),
),
),
)
.awaitTransactionSuccessAsync();
}
public async fillLimitOrderAsync(
order: LimitOrder,
opts: Partial<{
fillAmount: BigNumber | number;
taker: string;
protocolFee: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker, protocolFee } = {
taker: this.taker,
fillAmount: order.takerAmount,
...opts,
};
await this.prepareBalancesForOrdersAsync([order], taker);
const value = protocolFee === undefined ? this.protocolFee : protocolFee;
return this.zeroEx
.fillLimitOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker, value });
}
public async fillRfqOrderAsync(
order: RfqOrder,
fillAmount: BigNumber | number = order.takerAmount,
taker: string = this.taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillRfqOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker });
}
public createLimitOrderFilledEventArgs(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExLimitOrderFilledEventArgs {
const {
makerTokenFilledAmount,
takerTokenFilledAmount,
takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO : this.protocolFee;
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
takerTokenFeeFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
feeRecipient: order.feeRecipient,
makerToken: order.makerToken,
takerToken: order.takerToken,
protocolFeePaid: protocolFee,
pool: order.pool,
};
}
public createRfqOrderFilledEventArgs(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExRfqOrderFilledEventArgs {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
order,
takerTokenFillAmount,
takerTokenAlreadyFilledAmount,
);
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
makerToken: order.makerToken,
takerToken: order.takerToken,
pool: order.pool,
};
}
}
/**
* Generate a random limit order.
@ -40,3 +215,105 @@ export function getRandomRfqOrder(fields: Partial<RfqOrderFields> = {}): RfqOrde
...fields,
});
}
/**
* Asserts the fields of an OrderInfo object.
*/
export function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void {
expect(actual.status, 'Order status').to.eq(expected.status);
expect(actual.orderHash, 'Order hash').to.eq(expected.orderHash);
expect(actual.takerTokenFilledAmount, 'Order takerTokenFilledAmount').to.bignumber.eq(
expected.takerTokenFilledAmount,
);
}
/**
* Creates an order expiry field.
*/
export function createExpiry(deltaSeconds: number = 60): BigNumber {
return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds);
}
/**
* Computes the maker, taker, and taker token fee amounts filled for
* the given limit order.
*/
export function computeLimitOrderFilledAmounts(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): LimitOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
const takerTokenFeeFilledAmount = fillAmount
.times(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
takerTokenFeeFilledAmount,
};
}
/**
* Computes the maker and taker amounts filled for the given RFQ order.
*/
export function computeRfqOrderFilledAmounts(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): RfqOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
/**
* Computes the remaining fillable amount in maker token for
* the given order.
*/
export function getFillableMakerTokenAmount(
order: LimitOrder | RfqOrder,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
return order.takerAmount
.minus(takerTokenFilledAmount)
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
}
/**
* Computes the remaining fillable amnount in taker token, based on
* the amount already filled and the maker's balance/allowance.
*/
export function getActualFillableTakerTokenAmount(
order: LimitOrder | RfqOrder,
makerBalance: BigNumber = order.makerAmount,
makerAllowance: BigNumber = order.makerAmount,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount);
return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance)
.times(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_UP);
}

View File

@ -5,6 +5,7 @@
*/
export * from '../test/generated-wrappers/affiliate_fee_transformer';
export * from '../test/generated-wrappers/allowance_target';
export * from '../test/generated-wrappers/batch_fill_native_orders_feature';
export * from '../test/generated-wrappers/bootstrap_feature';
export * from '../test/generated-wrappers/bridge_adapter';
export * from '../test/generated-wrappers/bridge_source';
@ -20,6 +21,7 @@ export * from '../test/generated-wrappers/fixin_token_spender';
export * from '../test/generated-wrappers/flash_wallet';
export * from '../test/generated-wrappers/full_migration';
export * from '../test/generated-wrappers/i_allowance_target';
export * from '../test/generated-wrappers/i_batch_fill_native_orders_feature';
export * from '../test/generated-wrappers/i_bootstrap_feature';
export * from '../test/generated-wrappers/i_bridge_adapter';
export * from '../test/generated-wrappers/i_erc20_bridge';
@ -31,6 +33,8 @@ export * from '../test/generated-wrappers/i_liquidity_provider_feature';
export * from '../test/generated-wrappers/i_liquidity_provider_sandbox';
export * from '../test/generated-wrappers/i_meta_transactions_feature';
export * from '../test/generated-wrappers/i_mooniswap_pool';
export * from '../test/generated-wrappers/i_multiplex_feature';
export * from '../test/generated-wrappers/i_native_orders_events';
export * from '../test/generated-wrappers/i_native_orders_feature';
export * from '../test/generated-wrappers/i_ownable_feature';
export * from '../test/generated-wrappers/i_simple_function_registry_feature';
@ -39,6 +43,7 @@ export * from '../test/generated-wrappers/i_test_simple_function_registry_featur
export * from '../test/generated-wrappers/i_token_spender_feature';
export * from '../test/generated-wrappers/i_transform_erc20_feature';
export * from '../test/generated-wrappers/i_uniswap_feature';
export * from '../test/generated-wrappers/i_uniswap_v2_pair';
export * from '../test/generated-wrappers/i_zero_ex';
export * from '../test/generated-wrappers/initial_migration';
export * from '../test/generated-wrappers/lib_bootstrap';
@ -88,7 +93,12 @@ export * from '../test/generated-wrappers/mixin_uniswap';
export * from '../test/generated-wrappers/mixin_uniswap_v2';
export * from '../test/generated-wrappers/mixin_zero_ex_bridge';
export * from '../test/generated-wrappers/mooniswap_liquidity_provider';
export * from '../test/generated-wrappers/multiplex_feature';
export * from '../test/generated-wrappers/native_orders_cancellation';
export * from '../test/generated-wrappers/native_orders_feature';
export * from '../test/generated-wrappers/native_orders_info';
export * from '../test/generated-wrappers/native_orders_protocol_fees';
export * from '../test/generated-wrappers/native_orders_settlement';
export * from '../test/generated-wrappers/ownable_feature';
export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/permissionless_transformer_deployer';

View File

@ -4,6 +4,7 @@
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*", "./scripts/**/*"],
"files": [
"generated-artifacts/AffiliateFeeTransformer.json",
"generated-artifacts/BatchFillNativeOrdersFeature.json",
"generated-artifacts/BridgeAdapter.json",
"generated-artifacts/CurveLiquidityProvider.json",
"generated-artifacts/FeeCollector.json",
@ -11,9 +12,11 @@
"generated-artifacts/FillQuoteTransformer.json",
"generated-artifacts/FullMigration.json",
"generated-artifacts/IAllowanceTarget.json",
"generated-artifacts/IBatchFillNativeOrdersFeature.json",
"generated-artifacts/IERC20Transformer.json",
"generated-artifacts/IFlashWallet.json",
"generated-artifacts/ILiquidityProviderFeature.json",
"generated-artifacts/IMultiplexFeature.json",
"generated-artifacts/INativeOrdersFeature.json",
"generated-artifacts/IOwnableFeature.json",
"generated-artifacts/ISimpleFunctionRegistryFeature.json",
@ -24,6 +27,7 @@
"generated-artifacts/LiquidityProviderFeature.json",
"generated-artifacts/LogMetadataTransformer.json",
"generated-artifacts/MetaTransactionsFeature.json",
"generated-artifacts/MultiplexFeature.json",
"generated-artifacts/NativeOrdersFeature.json",
"generated-artifacts/OwnableFeature.json",
"generated-artifacts/PayTakerTransformer.json",
@ -35,6 +39,7 @@
"generated-artifacts/ZeroEx.json",
"test/generated-artifacts/AffiliateFeeTransformer.json",
"test/generated-artifacts/AllowanceTarget.json",
"test/generated-artifacts/BatchFillNativeOrdersFeature.json",
"test/generated-artifacts/BootstrapFeature.json",
"test/generated-artifacts/BridgeAdapter.json",
"test/generated-artifacts/BridgeSource.json",
@ -50,6 +55,7 @@
"test/generated-artifacts/FlashWallet.json",
"test/generated-artifacts/FullMigration.json",
"test/generated-artifacts/IAllowanceTarget.json",
"test/generated-artifacts/IBatchFillNativeOrdersFeature.json",
"test/generated-artifacts/IBootstrapFeature.json",
"test/generated-artifacts/IBridgeAdapter.json",
"test/generated-artifacts/IERC20Bridge.json",
@ -61,6 +67,8 @@
"test/generated-artifacts/ILiquidityProviderSandbox.json",
"test/generated-artifacts/IMetaTransactionsFeature.json",
"test/generated-artifacts/IMooniswapPool.json",
"test/generated-artifacts/IMultiplexFeature.json",
"test/generated-artifacts/INativeOrdersEvents.json",
"test/generated-artifacts/INativeOrdersFeature.json",
"test/generated-artifacts/IOwnableFeature.json",
"test/generated-artifacts/ISimpleFunctionRegistryFeature.json",
@ -69,6 +77,7 @@
"test/generated-artifacts/ITokenSpenderFeature.json",
"test/generated-artifacts/ITransformERC20Feature.json",
"test/generated-artifacts/IUniswapFeature.json",
"test/generated-artifacts/IUniswapV2Pair.json",
"test/generated-artifacts/IZeroEx.json",
"test/generated-artifacts/InitialMigration.json",
"test/generated-artifacts/LibBootstrap.json",
@ -118,7 +127,12 @@
"test/generated-artifacts/MixinUniswapV2.json",
"test/generated-artifacts/MixinZeroExBridge.json",
"test/generated-artifacts/MooniswapLiquidityProvider.json",
"test/generated-artifacts/MultiplexFeature.json",
"test/generated-artifacts/NativeOrdersCancellation.json",
"test/generated-artifacts/NativeOrdersFeature.json",
"test/generated-artifacts/NativeOrdersInfo.json",
"test/generated-artifacts/NativeOrdersProtocolFees.json",
"test/generated-artifacts/NativeOrdersSettlement.json",
"test/generated-artifacts/OwnableFeature.json",
"test/generated-artifacts/PayTakerTransformer.json",
"test/generated-artifacts/PermissionlessTransformerDeployer.json",

View File

@ -1,4 +1,13 @@
[
{
"version": "3.13.0",
"changes": [
{
"note": "Update IZeroEx artifact",
"pr": 140
}
]
},
{
"version": "3.12.0",
"changes": [

View File

@ -3,6 +3,16 @@
"contractName": "IZeroEx",
"compilerOutput": {
"abi": [
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "bytes32", "name": "orderHash", "type": "bytes32" },
{ "indexed": false, "internalType": "address", "name": "maker", "type": "address" },
{ "indexed": false, "internalType": "uint64", "name": "expiry", "type": "uint64" }
],
"name": "ExpiredRfqOrder",
"type": "event"
},
{
"anonymous": false,
"inputs": [
@ -39,26 +49,11 @@
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "contract IERC20TokenV06",
"name": "inputToken",
"type": "address"
},
{
"indexed": false,
"internalType": "contract IERC20TokenV06",
"name": "outputToken",
"type": "address"
},
{ "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" },
{ "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "inputTokenAmount", "type": "uint256" },
{ "indexed": false, "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" },
{
"indexed": false,
"internalType": "contract ILiquidityProvider",
"name": "provider",
"type": "address"
},
{ "indexed": false, "internalType": "address", "name": "provider", "type": "address" },
{ "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }
],
"name": "LiquidityProviderSwap",
@ -444,6 +439,127 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "contract IERC20TokenV06", "name": "inputToken", "type": "address" },
{ "internalType": "contract IERC20TokenV06", "name": "outputToken", "type": "address" },
{ "internalType": "uint256", "name": "sellAmount", "type": "uint256" },
{
"components": [
{ "internalType": "bytes4", "name": "selector", "type": "bytes4" },
{ "internalType": "uint256", "name": "sellAmount", "type": "uint256" },
{ "internalType": "bytes", "name": "data", "type": "bytes" }
],
"internalType": "struct IMultiplexFeature.WrappedBatchCall[]",
"name": "calls",
"type": "tuple[]"
}
],
"internalType": "struct IMultiplexFeature.BatchFillData",
"name": "fillData",
"type": "tuple"
},
{ "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" }
],
"name": "batchFill",
"outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "contract IERC20TokenV06", "name": "makerToken", "type": "address" },
{ "internalType": "contract IERC20TokenV06", "name": "takerToken", "type": "address" },
{ "internalType": "uint128", "name": "makerAmount", "type": "uint128" },
{ "internalType": "uint128", "name": "takerAmount", "type": "uint128" },
{ "internalType": "uint128", "name": "takerTokenFeeAmount", "type": "uint128" },
{ "internalType": "address", "name": "maker", "type": "address" },
{ "internalType": "address", "name": "taker", "type": "address" },
{ "internalType": "address", "name": "sender", "type": "address" },
{ "internalType": "address", "name": "feeRecipient", "type": "address" },
{ "internalType": "bytes32", "name": "pool", "type": "bytes32" },
{ "internalType": "uint64", "name": "expiry", "type": "uint64" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" }
],
"internalType": "struct LibNativeOrder.LimitOrder[]",
"name": "orders",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "enum LibSignature.SignatureType",
"name": "signatureType",
"type": "uint8"
},
{ "internalType": "uint8", "name": "v", "type": "uint8" },
{ "internalType": "bytes32", "name": "r", "type": "bytes32" },
{ "internalType": "bytes32", "name": "s", "type": "bytes32" }
],
"internalType": "struct LibSignature.Signature[]",
"name": "signatures",
"type": "tuple[]"
},
{ "internalType": "uint128[]", "name": "takerTokenFillAmounts", "type": "uint128[]" },
{ "internalType": "bool", "name": "revertIfIncomplete", "type": "bool" }
],
"name": "batchFillLimitOrders",
"outputs": [
{ "internalType": "uint128[]", "name": "takerTokenFilledAmounts", "type": "uint128[]" },
{ "internalType": "uint128[]", "name": "makerTokenFilledAmounts", "type": "uint128[]" }
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "contract IERC20TokenV06", "name": "makerToken", "type": "address" },
{ "internalType": "contract IERC20TokenV06", "name": "takerToken", "type": "address" },
{ "internalType": "uint128", "name": "makerAmount", "type": "uint128" },
{ "internalType": "uint128", "name": "takerAmount", "type": "uint128" },
{ "internalType": "address", "name": "maker", "type": "address" },
{ "internalType": "address", "name": "taker", "type": "address" },
{ "internalType": "address", "name": "txOrigin", "type": "address" },
{ "internalType": "bytes32", "name": "pool", "type": "bytes32" },
{ "internalType": "uint64", "name": "expiry", "type": "uint64" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" }
],
"internalType": "struct LibNativeOrder.RfqOrder[]",
"name": "orders",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "enum LibSignature.SignatureType",
"name": "signatureType",
"type": "uint8"
},
{ "internalType": "uint8", "name": "v", "type": "uint8" },
{ "internalType": "bytes32", "name": "r", "type": "bytes32" },
{ "internalType": "bytes32", "name": "s", "type": "bytes32" }
],
"internalType": "struct LibSignature.Signature[]",
"name": "signatures",
"type": "tuple[]"
},
{ "internalType": "uint128[]", "name": "takerTokenFillAmounts", "type": "uint128[]" },
{ "internalType": "bool", "name": "revertIfIncomplete", "type": "bool" }
],
"name": "batchFillRfqOrders",
"outputs": [
{ "internalType": "uint128[]", "name": "takerTokenFilledAmounts", "type": "uint128[]" },
{ "internalType": "uint128[]", "name": "makerTokenFilledAmounts", "type": "uint128[]" }
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
@ -1221,6 +1337,33 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "address[]", "name": "tokens", "type": "address[]" },
{ "internalType": "uint256", "name": "sellAmount", "type": "uint256" },
{
"components": [
{ "internalType": "bytes4", "name": "selector", "type": "bytes4" },
{ "internalType": "bytes", "name": "data", "type": "bytes" }
],
"internalType": "struct IMultiplexFeature.WrappedMultiHopCall[]",
"name": "calls",
"type": "tuple[]"
}
],
"internalType": "struct IMultiplexFeature.MultiHopFillData",
"name": "fillData",
"type": "tuple"
},
{ "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" }
],
"name": "multiHopFill",
"outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
@ -1402,6 +1545,40 @@
},
"returns": { "returnResults": "The ABI-encoded results of the underlying calls." }
},
"batchFill((address,address,uint256,(bytes4,uint256,bytes)[]),uint256)": {
"details": "Executes a batch of fills selling `fillData.inputToken` for `fillData.outputToken` in sequence. Refer to the internal variant `_batchFill` for the allowed nested operations.",
"params": {
"fillData": "Encodes the input/output tokens, the sell amount, and the nested operations for this batch fill.",
"minBuyAmount": "The minimum amount of `fillData.outputToken` to buy. Reverts if this amount is not met."
},
"returns": { "outputTokenAmount": "The amount of the output token bought." }
},
"batchFillLimitOrders((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)": {
"details": "Fills multiple limit orders.",
"params": {
"orders": "Array of limit orders.",
"revertIfIncomplete": "If true, reverts if this function fails to fill the full fill amount for any individual order.",
"signatures": "Array of signatures corresponding to each order.",
"takerTokenFillAmounts": "Array of desired amounts to fill each order."
},
"returns": {
"makerTokenFilledAmounts": "Array of amounts filled, in maker token.",
"takerTokenFilledAmounts": "Array of amounts filled, in taker token."
}
},
"batchFillRfqOrders((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)": {
"details": "Fills multiple RFQ orders.",
"params": {
"orders": "Array of RFQ orders.",
"revertIfIncomplete": "If true, reverts if this function fails to fill the full fill amount for any individual order.",
"signatures": "Array of signatures corresponding to each order.",
"takerTokenFillAmounts": "Array of desired amounts to fill each order."
},
"returns": {
"makerTokenFilledAmounts": "Array of amounts filled, in maker token.",
"takerTokenFilledAmounts": "Array of amounts filled, in taker token."
}
},
"batchGetLimitOrderRelevantStates((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[])": {
"details": "Batch version of `getLimitOrderRelevantState()`, without reverting. Orders that would normally cause `getLimitOrderRelevantState()` to revert will have empty results.",
"params": { "orders": "The limit orders.", "signatures": "The order signatures." },
@ -1600,6 +1777,14 @@
"target": "The migrator contract address."
}
},
"multiHopFill((address[],uint256,(bytes4,bytes)[]),uint256)": {
"details": "Executes a sequence of fills \"hopping\" through the path of tokens given by `fillData.tokens`. Refer to the internal variant `_multiHopFill` for the allowed nested operations.",
"params": {
"fillData": "Encodes the path of tokens, the sell amount, and the nested operations for this multi-hop fill.",
"minBuyAmount": "The minimum amount of the output token to buy. Reverts if this amount is not met."
},
"returns": { "outputTokenAmount": "The amount of the output token bought." }
},
"owner()": {
"details": "The owner of this contract.",
"returns": { "ownerAddress": "The owner address." }

View File

@ -1,4 +1,13 @@
[
{
"version": "13.14.0",
"changes": [
{
"note": "Update IZeroExContract wrapper",
"pr": 140
}
]
},
{
"version": "13.13.0",
"changes": [

View File

@ -36,6 +36,7 @@ import * as ethers from 'ethers';
// tslint:enable:no-unused-variable
export type IZeroExEventArgs =
| IZeroExExpiredRfqOrderEventArgs
| IZeroExLimitOrderFilledEventArgs
| IZeroExLiquidityProviderSwapEventArgs
| IZeroExMetaTransactionExecutedEventArgs
@ -52,6 +53,7 @@ export type IZeroExEventArgs =
| IZeroExTransformerDeployerUpdatedEventArgs;
export enum IZeroExEvents {
ExpiredRfqOrder = 'ExpiredRfqOrder',
LimitOrderFilled = 'LimitOrderFilled',
LiquidityProviderSwap = 'LiquidityProviderSwap',
MetaTransactionExecuted = 'MetaTransactionExecuted',
@ -68,6 +70,12 @@ export enum IZeroExEvents {
TransformerDeployerUpdated = 'TransformerDeployerUpdated',
}
export interface IZeroExExpiredRfqOrderEventArgs extends DecodedLogArgs {
orderHash: string;
maker: string;
expiry: BigNumber;
}
export interface IZeroExLimitOrderFilledEventArgs extends DecodedLogArgs {
orderHash: string;
maker: string;
@ -284,6 +292,29 @@ export class IZeroExContract extends BaseContract {
*/
public static ABI(): ContractAbi {
const abi = [
{
anonymous: false,
inputs: [
{
name: 'orderHash',
type: 'bytes32',
indexed: false,
},
{
name: 'maker',
type: 'address',
indexed: false,
},
{
name: 'expiry',
type: 'uint64',
indexed: false,
},
],
name: 'ExpiredRfqOrder',
outputs: [],
type: 'event',
},
{
anonymous: false,
inputs: [
@ -1193,6 +1224,253 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
name: 'fillData',
type: 'tuple',
components: [
{
name: 'inputToken',
type: 'address',
},
{
name: 'outputToken',
type: 'address',
},
{
name: 'sellAmount',
type: 'uint256',
},
{
name: 'calls',
type: 'tuple[]',
components: [
{
name: 'selector',
type: 'bytes4',
},
{
name: 'sellAmount',
type: 'uint256',
},
{
name: 'data',
type: 'bytes',
},
],
},
],
},
{
name: 'minBuyAmount',
type: 'uint256',
},
],
name: 'batchFill',
outputs: [
{
name: 'outputTokenAmount',
type: 'uint256',
},
],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
name: 'orders',
type: 'tuple[]',
components: [
{
name: 'makerToken',
type: 'address',
},
{
name: 'takerToken',
type: 'address',
},
{
name: 'makerAmount',
type: 'uint128',
},
{
name: 'takerAmount',
type: 'uint128',
},
{
name: 'takerTokenFeeAmount',
type: 'uint128',
},
{
name: 'maker',
type: 'address',
},
{
name: 'taker',
type: 'address',
},
{
name: 'sender',
type: 'address',
},
{
name: 'feeRecipient',
type: 'address',
},
{
name: 'pool',
type: 'bytes32',
},
{
name: 'expiry',
type: 'uint64',
},
{
name: 'salt',
type: 'uint256',
},
],
},
{
name: 'signatures',
type: 'tuple[]',
components: [
{
name: 'signatureType',
type: 'uint8',
},
{
name: 'v',
type: 'uint8',
},
{
name: 'r',
type: 'bytes32',
},
{
name: 's',
type: 'bytes32',
},
],
},
{
name: 'takerTokenFillAmounts',
type: 'uint128[]',
},
{
name: 'revertIfIncomplete',
type: 'bool',
},
],
name: 'batchFillLimitOrders',
outputs: [
{
name: 'takerTokenFilledAmounts',
type: 'uint128[]',
},
{
name: 'makerTokenFilledAmounts',
type: 'uint128[]',
},
],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
name: 'orders',
type: 'tuple[]',
components: [
{
name: 'makerToken',
type: 'address',
},
{
name: 'takerToken',
type: 'address',
},
{
name: 'makerAmount',
type: 'uint128',
},
{
name: 'takerAmount',
type: 'uint128',
},
{
name: 'maker',
type: 'address',
},
{
name: 'taker',
type: 'address',
},
{
name: 'txOrigin',
type: 'address',
},
{
name: 'pool',
type: 'bytes32',
},
{
name: 'expiry',
type: 'uint64',
},
{
name: 'salt',
type: 'uint256',
},
],
},
{
name: 'signatures',
type: 'tuple[]',
components: [
{
name: 'signatureType',
type: 'uint8',
},
{
name: 'v',
type: 'uint8',
},
{
name: 'r',
type: 'bytes32',
},
{
name: 's',
type: 'bytes32',
},
],
},
{
name: 'takerTokenFillAmounts',
type: 'uint128[]',
},
{
name: 'revertIfIncomplete',
type: 'bool',
},
],
name: 'batchFillRfqOrders',
outputs: [
{
name: 'takerTokenFilledAmounts',
type: 'uint128[]',
},
{
name: 'makerTokenFilledAmounts',
type: 'uint128[]',
},
],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
@ -2804,6 +3082,51 @@ export class IZeroExContract extends BaseContract {
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
name: 'fillData',
type: 'tuple',
components: [
{
name: 'tokens',
type: 'address[]',
},
{
name: 'sellAmount',
type: 'uint256',
},
{
name: 'calls',
type: 'tuple[]',
components: [
{
name: 'selector',
type: 'bytes4',
},
{
name: 'data',
type: 'bytes',
},
],
},
],
},
{
name: 'minBuyAmount',
type: 'uint256',
},
],
name: 'multiHopFill',
outputs: [
{
name: 'outputTokenAmount',
type: 'uint256',
},
],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [],
name: 'owner',
@ -3705,6 +4028,240 @@ export class IZeroExContract extends BaseContract {
},
};
}
/**
* Executes a batch of fills selling `fillData.inputToken`
* for `fillData.outputToken` in sequence. Refer to the
* internal variant `_batchFill` for the allowed nested
* operations.
* @param fillData Encodes the input/output tokens, the sell amount, and
* the nested operations for this batch fill.
* @param minBuyAmount The minimum amount of `fillData.outputToken` to
* buy. Reverts if this amount is not met.
*/
public batchFill(
fillData: {
inputToken: string;
outputToken: string;
sellAmount: BigNumber;
calls: Array<{ selector: string; sellAmount: BigNumber; data: string }>;
},
minBuyAmount: BigNumber,
): ContractTxFunctionObj<BigNumber> {
const self = (this as any) as IZeroExContract;
assert.isBigNumber('minBuyAmount', minBuyAmount);
const functionSignature = 'batchFill((address,address,uint256,(bytes4,uint256,bytes)[]),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, [fillData, minBuyAmount]);
},
};
}
/**
* Fills multiple limit orders.
* @param orders Array of limit orders.
* @param signatures Array of signatures corresponding to each order.
* @param takerTokenFillAmounts Array of desired amounts to fill each order.
* @param revertIfIncomplete If true, reverts if this function fails to
* fill the full fill amount for any individual order.
*/
public batchFillLimitOrders(
orders: Array<{
makerToken: string;
takerToken: string;
makerAmount: BigNumber;
takerAmount: BigNumber;
takerTokenFeeAmount: BigNumber;
maker: string;
taker: string;
sender: string;
feeRecipient: string;
pool: string;
expiry: BigNumber;
salt: BigNumber;
}>,
signatures: Array<{ signatureType: number | BigNumber; v: number | BigNumber; r: string; s: string }>,
takerTokenFillAmounts: BigNumber[],
revertIfIncomplete: boolean,
): ContractTxFunctionObj<[BigNumber[], BigNumber[]]> {
const self = (this as any) as IZeroExContract;
assert.isArray('orders', orders);
assert.isArray('signatures', signatures);
assert.isArray('takerTokenFillAmounts', takerTokenFillAmounts);
assert.isBoolean('revertIfIncomplete', revertIfIncomplete);
const functionSignature =
'batchFillLimitOrders((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)';
return {
async sendTransactionAsync(
txData?: Partial<TxData> | undefined,
opts: SendTransactionOpts = { shouldValidate: true },
): Promise<string> {
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync(
{ data: this.getABIEncodedTransactionData(), ...txData },
this.estimateGasAsync.bind(this),
);
if (opts.shouldValidate !== false) {
await this.callAsync(txDataWithDefaults);
}
return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
},
awaitTransactionSuccessAsync(
txData?: Partial<TxData>,
opts: AwaitTransactionSuccessOpts = { shouldValidate: true },
): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> {
return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts);
},
async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> {
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({
data: this.getABIEncodedTransactionData(),
...txData,
});
return self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
},
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<[BigNumber[], 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[], BigNumber[]]>(rawCallResult);
},
getABIEncodedTransactionData(): string {
return self._strictEncodeArguments(functionSignature, [
orders,
signatures,
takerTokenFillAmounts,
revertIfIncomplete,
]);
},
};
}
/**
* Fills multiple RFQ orders.
* @param orders Array of RFQ orders.
* @param signatures Array of signatures corresponding to each order.
* @param takerTokenFillAmounts Array of desired amounts to fill each order.
* @param revertIfIncomplete If true, reverts if this function fails to
* fill the full fill amount for any individual order.
*/
public batchFillRfqOrders(
orders: Array<{
makerToken: string;
takerToken: string;
makerAmount: BigNumber;
takerAmount: BigNumber;
maker: string;
taker: string;
txOrigin: string;
pool: string;
expiry: BigNumber;
salt: BigNumber;
}>,
signatures: Array<{ signatureType: number | BigNumber; v: number | BigNumber; r: string; s: string }>,
takerTokenFillAmounts: BigNumber[],
revertIfIncomplete: boolean,
): ContractTxFunctionObj<[BigNumber[], BigNumber[]]> {
const self = (this as any) as IZeroExContract;
assert.isArray('orders', orders);
assert.isArray('signatures', signatures);
assert.isArray('takerTokenFillAmounts', takerTokenFillAmounts);
assert.isBoolean('revertIfIncomplete', revertIfIncomplete);
const functionSignature =
'batchFillRfqOrders((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)';
return {
async sendTransactionAsync(
txData?: Partial<TxData> | undefined,
opts: SendTransactionOpts = { shouldValidate: true },
): Promise<string> {
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync(
{ data: this.getABIEncodedTransactionData(), ...txData },
this.estimateGasAsync.bind(this),
);
if (opts.shouldValidate !== false) {
await this.callAsync(txDataWithDefaults);
}
return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
},
awaitTransactionSuccessAsync(
txData?: Partial<TxData>,
opts: AwaitTransactionSuccessOpts = { shouldValidate: true },
): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> {
return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts);
},
async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> {
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({
data: this.getABIEncodedTransactionData(),
...txData,
});
return self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
},
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<[BigNumber[], 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[], BigNumber[]]>(rawCallResult);
},
getABIEncodedTransactionData(): string {
return self._strictEncodeArguments(functionSignature, [
orders,
signatures,
takerTokenFillAmounts,
revertIfIncomplete,
]);
},
};
}
/**
* Batch version of `getLimitOrderRelevantState()`, without reverting.
* Orders that would normally cause `getLimitOrderRelevantState()`
@ -5679,6 +6236,67 @@ export class IZeroExContract extends BaseContract {
},
};
}
/**
* Executes a sequence of fills "hopping" through the
* path of tokens given by `fillData.tokens`. Refer to the
* internal variant `_multiHopFill` for the allowed nested
* operations.
* @param fillData Encodes the path of tokens, the sell amount, and the
* nested operations for this multi-hop fill.
* @param minBuyAmount The minimum amount of the output token to buy.
* Reverts if this amount is not met.
*/
public multiHopFill(
fillData: { tokens: string[]; sellAmount: BigNumber; calls: Array<{ selector: string; data: string }> },
minBuyAmount: BigNumber,
): ContractTxFunctionObj<BigNumber> {
const self = (this as any) as IZeroExContract;
assert.isBigNumber('minBuyAmount', minBuyAmount);
const functionSignature = 'multiHopFill((address[],uint256,(bytes4,bytes)[]),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, [fillData, minBuyAmount]);
},
};
}
/**
* The owner of this contract.
*/

View File

@ -125,6 +125,7 @@ export {
IZeroExContract,
IZeroExEventArgs,
IZeroExEvents,
IZeroExExpiredRfqOrderEventArgs,
IZeroExLiquidityProviderSwapEventArgs,
IZeroExMetaTransactionExecutedEventArgs,
IZeroExMigratedEventArgs,

View File

@ -110,6 +110,20 @@ export class OnlyOrderMakerAllowed extends RevertError {
}
}
export class BatchFillIncompleteError extends RevertError {
constructor(orderHash?: string, takerTokenFilledAmount?: Numberish, takerTokenFillAmount?: Numberish) {
super(
'BatchFillIncompleteError',
'BatchFillIncompleteError(bytes32 orderHash, uint256 takerTokenFilledAmount, uint256 takerTokenFillAmount)',
{
orderHash,
takerTokenFilledAmount,
takerTokenFillAmount,
},
);
}
}
const types = [
ProtocolFeeRefundFailed,
OrderNotFillableByOriginError,
@ -120,6 +134,7 @@ const types = [
CancelSaltTooLowError,
FillOrKillFailedError,
OnlyOrderMakerAllowed,
BatchFillIncompleteError,
];
// Register the types we've defined.