Feat/multiplex/v2 (#263)

* Refactor Multiplex into multiple files

* Pull UniswapV3 into separate file

* Add support for multihop nested within batch sell

* Add useSelfBalance and recipient to _fillRfqOrder

* Expose onlySelf variant in UniswapV3Feature for Multiplex

* Add useSelfBalance and recipient to _transformERC20

* Add support for proportional fill amounts in batchSell

* Comments and renaming

* Unit tests

* Use caps for immutables

* Rename taker -> recipient in TransformContext and SettleOrderInfo

* lint

* Address nits

* Swallow reverts for LiquidityProvider and UniswapV2 batch sells

* Address spot-check findings (#279)

* Check didSucceed in _callWithOptionalBooleanResult

* Add takerToken=ETH support to OtcOrdersFeature (#287)

* Add takerToken=ETH support to OtcOrdersFeature

* Add batchFillTakerSignedOtcOrders

* Add support for OTC to Multiplex

* Address PR feedback

* Update TransformERC20Feature (#303)

* remove multiplex_utils

* Update changelog

* unbreak tests
This commit is contained in:
mzhu25 2021-08-12 17:09:46 -07:00 committed by GitHub
parent 692231c2ea
commit b46eeadc64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 5092 additions and 2101 deletions

View File

@ -28,7 +28,7 @@ library LibERC20TokenV06 {
bytes constant private DECIMALS_CALL_DATA = hex"313ce567";
/// @dev Calls `IERC20TokenV06(token).approve()`.
/// Reverts if the result fails `isSuccessfulResult()` or the call reverts.
/// Reverts if the return data is invalid or the call reverts.
/// @param token The address of the token contract.
/// @param spender The address that receives an allowance.
/// @param allowance The allowance to set.
@ -49,7 +49,7 @@ library LibERC20TokenV06 {
/// @dev Calls `IERC20TokenV06(token).approve()` and sets the allowance to the
/// maximum if the current approval is not already >= an amount.
/// Reverts if the result fails `isSuccessfulResult()` or the call reverts.
/// Reverts if the return data is invalid or the call reverts.
/// @param token The address of the token contract.
/// @param spender The address that receives an allowance.
/// @param amount The minimum allowance needed.
@ -66,7 +66,7 @@ library LibERC20TokenV06 {
}
/// @dev Calls `IERC20TokenV06(token).transfer()`.
/// Reverts if the result fails `isSuccessfulResult()` or the call reverts.
/// Reverts if the return data is invalid or the call reverts.
/// @param token The address of the token contract.
/// @param to The address that receives the tokens
/// @param amount Number of tokens to transfer.
@ -86,7 +86,7 @@ library LibERC20TokenV06 {
}
/// @dev Calls `IERC20TokenV06(token).transferFrom()`.
/// Reverts if the result fails `isSuccessfulResult()` or the call reverts.
/// Reverts if the return data is invalid or the call reverts.
/// @param token The address of the token contract.
/// @param from The owner of the tokens.
/// @param to The address that receives the tokens
@ -168,27 +168,6 @@ library LibERC20TokenV06 {
}
}
/// @dev Check if the data returned by a non-static call to an ERC20 token
/// is a successful result. Supported functions are `transfer()`,
/// `transferFrom()`, and `approve()`.
/// @param resultData The raw data returned by a non-static call to the ERC20 token.
/// @return isSuccessful Whether the result data indicates success.
function isSuccessfulResult(bytes memory resultData)
internal
pure
returns (bool isSuccessful)
{
if (resultData.length == 0) {
return true;
}
if (resultData.length >= 32) {
uint256 result = LibBytesV06.readUint256(resultData, 0);
if (result == 1) {
return true;
}
}
}
/// @dev Executes a call on address `target` with calldata `callData`
/// and asserts that either nothing was returned or a single boolean
/// was returned equal to `true`.
@ -201,9 +180,31 @@ library LibERC20TokenV06 {
private
{
(bool didSucceed, bytes memory resultData) = target.call(callData);
if (didSucceed && isSuccessfulResult(resultData)) {
// Revert if the call reverted.
if (!didSucceed) {
LibRichErrorsV06.rrevert(resultData);
}
// If we get back 0 returndata, this may be a non-standard ERC-20 that
// does not return a boolean. Check that it at least contains code.
if (resultData.length == 0) {
uint256 size;
assembly { size := extcodesize(target) }
require(size > 0, "invalid token address, contains no code");
return;
}
// If we get back at least 32 bytes, we know the target address
// contains code, and we assume it is a token that returned a boolean
// success value, which must be true.
if (resultData.length >= 32) {
uint256 result = LibBytesV06.readUint256(resultData, 0);
if (result == 1) {
return;
} else {
LibRichErrorsV06.rrevert(resultData);
}
}
// If 0 < returndatasize < 32, the target is a contract, but not a
// valid token.
LibRichErrorsV06.rrevert(resultData);
}
}

View File

@ -40,6 +40,6 @@ export function verifyEventsFromLogs<TEventArgs>(
const _logs = filterLogsToArguments<TEventArgs>(logs, eventName);
expect(_logs.length, `Number of ${eventName} events emitted`).to.eq(expectedEvents.length);
_logs.forEach((log, index) => {
expect(log, `${eventName} event ${index}`).to.deep.equal(expectedEvents[index]);
expect(log, `${eventName} event ${index}`).to.deep.equal({ ...log, ...expectedEvents[index] });
});
}

View File

@ -21,13 +21,10 @@ pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "./IStaking.sol";
contract DefaultPoolOperator {
using LibERC20TokenV06 for IERC20TokenV06;
// Immutables
IStaking public immutable stakingProxy;
IERC20TokenV06 public immutable weth;
@ -57,7 +54,7 @@ contract DefaultPoolOperator {
function returnStakingRewards()
external
{
uint256 wethBalance = weth.compatBalanceOf(address(this));
weth.compatTransfer(address(stakingProxy), wethBalance);
uint256 wethBalance = weth.balanceOf(address(this));
weth.transfer(address(stakingProxy), wethBalance);
}
}

View File

@ -1,4 +1,25 @@
[
{
"version": "0.28.0",
"changes": [
{
"note": "Transfer output tokens in TransformERC20Feature",
"pr": 279
},
{
"note": "Add support for takerToken=0xeee... in OtcOrdersFeature",
"pr": 287
},
{
"note": "Add support for OTC orders in MultiplexFeature",
"pr": 287
},
{
"note": "Multiplex v2: Refactor into multiple files, add ETH support, and other miscellanea",
"pr": 263
}
]
},
{
"timestamp": 1628665757,
"version": "0.27.1",

View File

@ -88,23 +88,6 @@ library LibNativeOrdersRichErrors {
);
}
function OrderNotSignedByTakerError(
bytes32 orderHash,
address signer,
address taker
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("OrderNotSignedByTakerError(bytes32,address,address)")),
orderHash,
signer,
taker
);
}
function InvalidSignerError(
address maker,
address signer

View File

@ -48,7 +48,7 @@ contract BatchFillNativeOrdersFeature is
/// @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);
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0);
constructor(address zeroExAddress)
public
@ -170,6 +170,8 @@ contract BatchFillNativeOrdersFeature is
orders[i],
signatures[i],
takerTokenFillAmounts[i],
msg.sender,
false,
msg.sender
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)

View File

@ -78,7 +78,7 @@ contract MetaTransactionsFeature is
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "MetaTransactions";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 1);
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 2, 0);
/// @dev EIP712 typehash of the `MetaTransactionData` struct.
bytes32 public immutable MTX_EIP712_TYPEHASH = keccak256(
"MetaTransactionData("
@ -415,7 +415,9 @@ contract MetaTransactionsFeature is
outputToken: args.outputToken,
inputTokenAmount: args.inputTokenAmount,
minOutputTokenAmount: args.minOutputTokenAmount,
transformations: args.transformations
transformations: args.transformations,
useSelfBalance: false,
recipient: state.mtx.signer
})
),
state.mtx.value
@ -498,7 +500,9 @@ contract MetaTransactionsFeature is
order,
signature,
takerTokenFillAmount,
state.mtx.signer // taker is mtx signer
state.mtx.signer, // taker is mtx signer
false,
state.mtx.signer
),
state.mtx.value
);

View File

@ -1,820 +0,0 @@
// 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 "./interfaces/IUniswapV3Feature.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, 1, 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_
)
public
FixinEIP712(zeroExAddress)
{
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 == IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector) {
(bool success, bytes memory resultData) = address(this).delegatecall(
abi.encodeWithSelector(
IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector,
wrappedCall.data,
inputTokenAmount,
0,
msg.sender
)
);
if (success) {
uint256 outputTokenAmount_ = abi.decode(resultData, (uint256));
// 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.
_transferERC20TokensFrom(
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 {
_transferERC20TokensFrom(
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.
_transferERC20TokensFrom(
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);
_transferERC20TokensFrom(
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

@ -34,7 +34,7 @@ contract NativeOrdersFeature is
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "LimitOrders";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 2, 0);
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 3, 0);
constructor(
address zeroExAddress,

View File

@ -47,21 +47,12 @@ contract OtcOrdersFeature is
using LibSafeMathV06 for uint256;
using LibSafeMathV06 for uint128;
/// @dev Options for handling ETH/WETH conversion
/// @param LeaveAsWeth Neither unwrap nor wrap.
/// @param WrapEth Wrap attached ETH.
/// @param UnwrapWeth Unwrap WETH before transferring
/// to taker.
enum WethOptions {
LeaveAsWeth,
WrapEth,
UnwrapWeth
}
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "OtcOrders";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev ETH pseudo-token address.
address constant private ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @dev The WETH token contract.
IEtherTokenV06 private immutable WETH;
@ -80,8 +71,12 @@ contract OtcOrdersFeature is
returns (bytes4 success)
{
_registerFeatureFunction(this.fillOtcOrder.selector);
_registerFeatureFunction(this.fillOtcOrderForEth.selector);
_registerFeatureFunction(this.fillOtcOrderWithEth.selector);
_registerFeatureFunction(this.fillTakerSignedOtcOrderForEth.selector);
_registerFeatureFunction(this.fillTakerSignedOtcOrder.selector);
_registerFeatureFunction(this.batchFillTakerSignedOtcOrders.selector);
_registerFeatureFunction(this._fillOtcOrder.selector);
_registerFeatureFunction(this.getOtcOrderInfo.selector);
_registerFeatureFunction(this.getOtcOrderHash.selector);
_registerFeatureFunction(this.lastOtcTxOriginNonce.selector);
@ -93,36 +88,90 @@ contract OtcOrdersFeature is
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @param unwrapWeth Whether or not to unwrap bought WETH into ETH
/// before transferring it to the taker. Should be set to false
/// if the maker token is not WETH.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOtcOrder(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
uint128 takerTokenFillAmount,
bool unwrapWeth
uint128 takerTokenFillAmount
)
public
override
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
if (!_isSenderValidTaker(order.taker)) {
bytes32 orderHash = getOtcOrderHash(order);
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderHash,
msg.sender,
order.taker
).rrevert();
}
LibSignature.Signature memory nullSignature;
return _fillOtcOrderPrivate(
order,
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
nullSignature,
msg.sender
);
(takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder(
order,
takerTokenFillAmount,
unwrapWeth ? WethOptions.UnwrapWeth : WethOptions.LeaveAsWeth
msg.sender,
msg.sender
);
emit OtcOrderFilled(
orderInfo.orderHash,
order.maker,
msg.sender,
address(order.makerToken),
address(order.takerToken),
makerTokenFilledAmount,
takerTokenFilledAmount
);
}
/// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens.
/// Unwraps bought WETH into ETH. before sending it to
/// the taker.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOtcOrderForEth(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
uint128 takerTokenFillAmount
)
public
override
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
require(
order.makerToken == WETH,
"OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH"
);
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
msg.sender
);
(takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder(
order,
takerTokenFillAmount,
msg.sender,
address(this)
);
// Unwrap WETH
WETH.withdraw(makerTokenFilledAmount);
// Transfer ETH to taker
_transferEth(msg.sender, makerTokenFilledAmount);
emit OtcOrderFilled(
orderInfo.orderHash,
order.maker,
msg.sender,
address(order.makerToken),
address(order.takerToken),
makerTokenFilledAmount,
takerTokenFilledAmount
);
}
@ -141,21 +190,47 @@ contract OtcOrdersFeature is
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
if (!_isSenderValidTaker(order.taker)) {
bytes32 orderHash = getOtcOrderHash(order);
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderHash,
msg.sender,
order.taker
).rrevert();
if (order.takerToken == WETH) {
// Wrap ETH
WETH.deposit{value: msg.value}();
} else {
require(
address(order.takerToken) == ETH_TOKEN_ADDRESS,
"OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN"
);
}
LibSignature.Signature memory nullSignature;
return _fillOtcOrderPrivate(
order,
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
nullSignature,
msg.sender
);
(takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder(
order,
msg.value.safeDowncastToUint128(),
WethOptions.WrapEth
address(this),
msg.sender
);
if (takerTokenFilledAmount < msg.value) {
uint256 refundAmount = msg.value - uint256(takerTokenFilledAmount);
if (order.takerToken == WETH) {
WETH.withdraw(refundAmount);
}
// Refund unused ETH
_transferEth(msg.sender, refundAmount);
}
emit OtcOrderFilled(
orderInfo.orderHash,
order.maker,
msg.sender,
address(order.makerToken),
address(order.takerToken),
makerTokenFilledAmount,
takerTokenFilledAmount
);
}
@ -164,112 +239,28 @@ contract OtcOrdersFeature is
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerSignature The order signature from the taker.
/// @param unwrapWeth Whether or not to unwrap bought WETH into ETH
/// before transferring it to the taker. Should be set to false
/// if the maker token is not WETH.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillTakerSignedOtcOrder(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
LibSignature.Signature memory takerSignature,
bool unwrapWeth
LibSignature.Signature memory takerSignature
)
public
override
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
return _fillOtcOrderPrivate(
order,
makerSignature,
takerSignature,
order.takerAmount,
unwrapWeth ? WethOptions.UnwrapWeth : WethOptions.LeaveAsWeth
);
}
/// @dev Fill an OTC order. Private variant.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerSignature The order signature from the taker.
/// Ignored if msg.sender == order.taker.
/// @param takerTokenFillAmount Maximum taker token amount to
/// fill this order with.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillOtcOrderPrivate(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
LibSignature.Signature memory takerSignature,
uint128 takerTokenFillAmount,
WethOptions wethOptions
)
private
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
// Must be fillable.
if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
LibNativeOrdersRichErrors.OrderNotFillableError(
orderInfo.orderHash,
uint8(orderInfo.status)
).rrevert();
}
address taker = msg.sender;
{
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();
}
// Maker signature must be valid for the order.
address makerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, makerSignature);
if (
makerSigner != order.maker &&
!stor.orderSignerRegistry[order.maker][makerSigner]
) {
LibNativeOrdersRichErrors.OrderNotSignedByMakerError(
orderInfo.orderHash,
makerSigner,
order.maker
).rrevert();
}
// If msg.sender is not the taker, validate the taker signature.
if (!_isSenderValidTaker(order.taker)) {
address takerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature);
if (
takerSigner != order.taker &&
!stor.orderSignerRegistry[order.taker][takerSigner]
) {
LibNativeOrdersRichErrors.OrderNotSignedByTakerError(
orderInfo.orderHash,
takerSigner,
order.taker
).rrevert();
}
taker = order.taker;
}
}
// Settle between the maker and taker.
(takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder(
address taker = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
taker
);
_settleOtcOrder(
order,
order.takerAmount,
taker,
takerTokenFillAmount,
wethOptions
taker
);
emit OtcOrderFilled(
@ -278,22 +269,227 @@ contract OtcOrdersFeature is
taker,
address(order.makerToken),
address(order.takerToken),
takerTokenFilledAmount,
makerTokenFilledAmount
order.makerAmount,
order.takerAmount
);
}
/// @dev Fully fill an OTC order. "Meta-transaction" variant,
/// requires order to be signed by both maker and taker.
/// Unwraps bought WETH into ETH. before sending it to
/// the taker.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerSignature The order signature from the taker.
function fillTakerSignedOtcOrderForEth(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
LibSignature.Signature memory takerSignature
)
public
override
{
require(
order.makerToken == WETH,
"OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH"
);
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
address taker = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
taker
);
_settleOtcOrder(
order,
order.takerAmount,
taker,
address(this)
);
// Unwrap WETH
WETH.withdraw(order.makerAmount);
// Transfer ETH to taker
_transferEth(taker, order.makerAmount);
emit OtcOrderFilled(
orderInfo.orderHash,
order.maker,
taker,
address(order.makerToken),
address(order.takerToken),
order.makerAmount,
order.takerAmount
);
}
/// @dev Fills multiple taker-signed OTC orders.
/// @param orders Array of OTC orders.
/// @param makerSignatures Array of maker signatures for each order.
/// @param takerSignatures Array of taker signatures for each order.
/// @param unwrapWeth Array of booleans representing whether or not
/// to unwrap bought WETH into ETH for each order. Should be set
/// to false if the maker token is not WETH.
/// @return successes Array of booleans representing whether or not
/// each order in `orders` was filled successfully.
function batchFillTakerSignedOtcOrders(
LibNativeOrder.OtcOrder[] memory orders,
LibSignature.Signature[] memory makerSignatures,
LibSignature.Signature[] memory takerSignatures,
bool[] memory unwrapWeth
)
public
override
returns (bool[] memory successes)
{
require(
orders.length == makerSignatures.length &&
orders.length == takerSignatures.length &&
orders.length == unwrapWeth.length,
"OtcOrdersFeature::batchFillTakerSignedOtcOrders/MISMATCHED_ARRAY_LENGTHS"
);
successes = new bool[](orders.length);
for (uint256 i = 0; i != orders.length; i++) {
bytes4 fnSelector = unwrapWeth[i]
? this.fillTakerSignedOtcOrderForEth.selector
: this.fillTakerSignedOtcOrder.selector;
// Swallow reverts
(successes[i], ) = _implementation.delegatecall(
abi.encodeWithSelector(
fnSelector,
orders[i],
makerSignatures[i],
takerSignatures[i]
)
);
}
}
/// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens.
/// Internal variant.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @param taker The address to fill the order in the context of.
/// @param useSelfBalance Whether to use the Exchange Proxy's balance
/// of input tokens.
/// @param recipient The recipient of the bought maker tokens.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillOtcOrder(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory makerSignature,
uint128 takerTokenFillAmount,
address taker,
bool useSelfBalance,
address recipient
)
public
override
onlySelf
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order);
_validateOtcOrder(
order,
orderInfo,
makerSignature,
taker
);
(takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder(
order,
takerTokenFillAmount,
useSelfBalance ? address(this) : taker,
recipient
);
emit OtcOrderFilled(
orderInfo.orderHash,
order.maker,
taker,
address(order.makerToken),
address(order.takerToken),
makerTokenFilledAmount,
takerTokenFilledAmount
);
}
/// @dev Validates an OTC order, reverting if the order cannot be
/// filled by the given taker.
/// @param order The OTC order.
/// @param orderInfo Info on the order.
/// @param makerSignature The order signature from the maker.
/// @param taker The order taker.
function _validateOtcOrder(
LibNativeOrder.OtcOrder memory order,
LibNativeOrder.OtcOrderInfo memory orderInfo,
LibSignature.Signature memory makerSignature,
address taker
)
private
view
{
// Must be fillable.
if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
LibNativeOrdersRichErrors.OrderNotFillableError(
orderInfo.orderHash,
uint8(orderInfo.status)
).rrevert();
}
// Must be a valid taker for the order.
if (order.taker != address(0) && order.taker != taker) {
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderInfo.orderHash,
taker,
order.taker
).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();
}
// Maker signature must be valid for the order.
address makerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, makerSignature);
if (
makerSigner != order.maker &&
!stor.orderSignerRegistry[order.maker][makerSigner]
) {
LibNativeOrdersRichErrors.OrderNotSignedByMakerError(
orderInfo.orderHash,
makerSigner,
order.maker
).rrevert();
}
}
/// @dev Settle the trade between an OTC order's maker and taker.
/// @param order The OTC order.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @param payer The address holding the taker tokens.
/// @param recipient The recipient of the maker tokens.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _settleOtcOrder(
LibNativeOrder.OtcOrder memory order,
address taker,
uint128 takerTokenFillAmount,
WethOptions wethOptions
address payer,
address recipient
)
private
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
@ -326,57 +522,34 @@ contract OtcOrdersFeature is
));
}
if (wethOptions == WethOptions.WrapEth) {
require(
order.takerToken == WETH,
"OtcOrdersFeature/INVALID_WRAP_ETH"
);
// Wrap ETH
WETH.deposit{value: takerTokenFilledAmount}();
// Transfer WETH to maker
WETH.transfer(order.maker, takerTokenFilledAmount);
if (takerTokenFilledAmount < msg.value) {
// Refund unused ETH
_transferEth(
msg.sender,
msg.value - uint256(takerTokenFilledAmount)
);
if (payer == address(this)) {
if (address(order.takerToken) == ETH_TOKEN_ADDRESS) {
// Transfer ETH to the maker.
payable(order.maker).transfer(takerTokenFilledAmount);
} else {
// Transfer this -> maker.
_transferERC20Tokens(
order.takerToken,
order.maker,
takerTokenFilledAmount
);
}
} else {
// Transfer taker -> maker
_transferERC20TokensFrom(
order.takerToken,
taker,
payer,
order.maker,
takerTokenFilledAmount
);
}
if (wethOptions == WethOptions.UnwrapWeth) {
require(
order.makerToken == WETH,
"OtcOrdersFeature/INVALID_UNWRAP_WETH"
);
// Transfer maker tokens in
_transferERC20TokensFrom(
order.makerToken,
order.maker,
address(this),
makerTokenFilledAmount
);
// Unwrap WETH
WETH.withdraw(makerTokenFilledAmount);
// Transfer ETH to taker
_transferEth(taker, makerTokenFilledAmount);
} else {
// Transfer maker -> taker.
_transferERC20TokensFrom(
order.makerToken,
order.maker,
taker,
makerTokenFilledAmount
);
}
// Transfer maker -> recipient.
_transferERC20TokensFrom(
order.makerToken,
order.maker,
recipient,
makerTokenFilledAmount
);
}
/// @dev Get the order info for an OTC order.
@ -461,12 +634,4 @@ contract OtcOrdersFeature is
revertData.rrevert();
}
}
function _isSenderValidTaker(address orderTaker)
private
view
returns (bool)
{
return orderTaker == address(0) || orderTaker == msg.sender;
}
}

View File

@ -22,6 +22,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
@ -51,16 +52,14 @@ contract TransformERC20Feature is
struct TransformERC20PrivateState {
IFlashWallet wallet;
address transformerDeployer;
uint256 takerOutputTokenBalanceBefore;
uint256 takerOutputTokenBalanceAfter;
uint256 recipientOutputTokenBalanceBefore;
uint256 recipientOutputTokenBalanceAfter;
}
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "TransformERC20";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 3, 1);
constructor() public {}
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 4, 0);
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
@ -76,7 +75,7 @@ contract TransformERC20Feature is
_registerFeatureFunction(this.setTransformerDeployer.selector);
_registerFeatureFunction(this.setQuoteSigner.selector);
_registerFeatureFunction(this.getQuoteSigner.selector);
_registerFeatureFunction(this.transformERC20.selector);
_registerFeatureFunction(this.transformERC20Staging.selector);
_registerFeatureFunction(this._transformERC20.selector);
if (this.getTransformWallet() == IFlashWallet(address(0))) {
// Create the transform wallet if it doesn't exist.
@ -146,6 +145,44 @@ contract TransformERC20Feature is
LibTransformERC20Storage.getStorage().wallet = wallet;
}
/// @dev Wrapper for `transformERC20`. This selector will be temporarily
/// registered to the Exchange Proxy so that we can migrate 0x API
/// with no downtime. Once 0x API has been updated to point to this
/// function, we can safely re-register `transformERC20`, point
/// 0x API back to `transformERC20`, and deregister this function.
/// @param inputToken The token being provided by the sender.
/// If `0xeee...`, ETH is implied and should be provided with the call.`
/// @param outputToken The token to be acquired by the sender.
/// `0xeee...` implies ETH.
/// @param inputTokenAmount The amount of `inputToken` to take from the sender.
/// If set to `uint256(-1)`, the entire spendable balance of the taker
/// will be solt.
/// @param minOutputTokenAmount The minimum amount of `outputToken` the sender
/// must receive for the entire transformation to succeed. If set to zero,
/// the minimum output token transfer will not be asserted.
/// @param transformations The transformations to execute on the token balance(s)
/// in sequence.
/// @return outputTokenAmount The amount of `outputToken` received by the sender.
function transformERC20Staging(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 minOutputTokenAmount,
Transformation[] memory transformations
)
public
payable
returns (uint256 outputTokenAmount)
{
return transformERC20(
inputToken,
outputToken,
inputTokenAmount,
minOutputTokenAmount,
transformations
);
}
/// @dev Executes a series of transformations to convert an ERC20 `inputToken`
/// to an ERC20 `outputToken`.
/// @param inputToken The token being provided by the sender.
@ -180,7 +217,9 @@ contract TransformERC20Feature is
outputToken: outputToken,
inputTokenAmount: inputTokenAmount,
minOutputTokenAmount: minOutputTokenAmount,
transformations: transformations
transformations: transformations,
useSelfBalance: false,
recipient: msg.sender
})
);
}
@ -208,7 +247,7 @@ contract TransformERC20Feature is
{
// If the input token amount is -1 and we are not selling ETH,
// transform the taker's entire spendable balance.
if (args.inputTokenAmount == uint256(-1)) {
if (!args.useSelfBalance && args.inputTokenAmount == uint256(-1)) {
if (LibERC20Transformer.isTokenETH(args.inputToken)) {
// We can't pull more ETH from the taker, so we just set the
// input token amount to the value attached to the call.
@ -225,17 +264,12 @@ contract TransformERC20Feature is
state.wallet = getTransformWallet();
state.transformerDeployer = getTransformerDeployer();
// Remember the initial output token balance of the taker.
state.takerOutputTokenBalanceBefore =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker);
// Remember the initial output token balance of the recipient.
state.recipientOutputTokenBalanceBefore =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient);
// Pull input tokens from the taker to the wallet and transfer attached ETH.
_transferInputTokensAndAttachedEth(
args.inputToken,
args.taker,
address(state.wallet),
args.inputTokenAmount
);
_transferInputTokensAndAttachedEth(args, address(state.wallet));
{
// Perform transformations.
@ -244,22 +278,29 @@ contract TransformERC20Feature is
state.wallet,
args.transformations[i],
state.transformerDeployer,
args.taker
args.recipient
);
}
// Transfer output tokens from wallet to recipient
outputTokenAmount = _executeOutputTokenTransfer(
args.outputToken,
state.wallet,
args.recipient
);
}
// Compute how much output token has been transferred to the taker.
state.takerOutputTokenBalanceAfter =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker);
if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) {
// Compute how much output token has been transferred to the recipient.
state.recipientOutputTokenBalanceAfter =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient);
if (state.recipientOutputTokenBalanceAfter < state.recipientOutputTokenBalanceBefore) {
LibTransformERC20RichErrors.NegativeTransformERC20OutputError(
address(args.outputToken),
state.takerOutputTokenBalanceBefore - state.takerOutputTokenBalanceAfter
state.recipientOutputTokenBalanceBefore - state.recipientOutputTokenBalanceAfter
).rrevert();
}
outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub(
state.takerOutputTokenBalanceBefore
outputTokenAmount = LibSafeMathV06.min256(
outputTokenAmount,
state.recipientOutputTokenBalanceAfter.safeSub(state.recipientOutputTokenBalanceBefore)
);
// Ensure enough output token has been sent to the taker.
if (outputTokenAmount < args.minOutputTokenAmount) {
@ -292,38 +333,49 @@ contract TransformERC20Feature is
return LibTransformERC20Storage.getStorage().wallet;
}
/// @dev Transfer input tokens from the taker and any attached ETH to `to`
/// @param inputToken The token to pull from the taker.
/// @param from The from (taker) address.
/// @dev Transfer input tokens and any attached ETH to `to`
/// @param args A `TransformERC20Args` struct.
/// @param to The recipient of tokens and ETH.
/// @param amount Amount of `inputToken` tokens to transfer.
function _transferInputTokensAndAttachedEth(
IERC20TokenV06 inputToken,
address from,
address payable to,
uint256 amount
TransformERC20Args memory args,
address payable to
)
private
{
if (
LibERC20Transformer.isTokenETH(args.inputToken) &&
msg.value < args.inputTokenAmount
) {
// Token is ETH, so the caller must attach enough ETH to the call.
LibTransformERC20RichErrors.InsufficientEthAttachedError(
msg.value,
args.inputTokenAmount
).rrevert();
}
// Transfer any attached ETH.
if (msg.value != 0) {
to.transfer(msg.value);
}
// Transfer input tokens.
if (!LibERC20Transformer.isTokenETH(inputToken) && amount != 0) {
// Token is not ETH, so pull ERC20 tokens.
_transferERC20TokensFrom(
inputToken,
from,
to,
amount
);
} else if (msg.value < amount) {
// Token is ETH, so the caller must attach enough ETH to the call.
LibTransformERC20RichErrors.InsufficientEthAttachedError(
msg.value,
amount
).rrevert();
if (!LibERC20Transformer.isTokenETH(args.inputToken)) {
if (args.useSelfBalance) {
// Use EP balance input token.
_transferERC20Tokens(
args.inputToken,
to,
args.inputTokenAmount
);
} else {
// Pull ERC20 tokens from taker.
_transferERC20TokensFrom(
args.inputToken,
args.taker,
to,
args.inputTokenAmount
);
}
}
}
@ -331,12 +383,12 @@ contract TransformERC20Feature is
/// @param wallet The wallet instance.
/// @param transformation The transformation.
/// @param transformerDeployer The address of the transformer deployer.
/// @param taker The taker address.
/// @param recipient The recipient address.
function _executeTransformation(
IFlashWallet wallet,
Transformation memory transformation,
address transformerDeployer,
address payable taker
address payable recipient
)
private
{
@ -354,7 +406,7 @@ contract TransformERC20Feature is
IERC20Transformer.transform.selector,
IERC20Transformer.TransformContext({
sender: msg.sender,
taker: taker,
recipient: recipient,
data: transformation.data
})
)
@ -370,4 +422,52 @@ contract TransformERC20Feature is
).rrevert();
}
}
function _executeOutputTokenTransfer(
IERC20TokenV06 outputToken,
IFlashWallet wallet,
address payable recipient
)
private
returns (uint256 transferAmount)
{
transferAmount =
LibERC20Transformer.getTokenBalanceOf(outputToken, address(wallet));
if (LibERC20Transformer.isTokenETH(outputToken)) {
wallet.executeCall(
recipient,
"",
transferAmount
);
} else {
bytes memory resultData = wallet.executeCall(
payable(address(outputToken)),
abi.encodeWithSelector(
IERC20TokenV06.transfer.selector,
recipient,
transferAmount
),
0
);
if (resultData.length == 0) {
// If we get back 0 returndata, this may be a non-standard ERC-20 that
// does not return a boolean. Check that it at least contains code.
uint256 size;
assembly { size := extcodesize(outputToken) }
require(size > 0, "invalid token address, contains no code");
} else if (resultData.length >= 32) {
// If we get back at least 32 bytes, we know the target address
// contains code, and we assume it is a token that returned a boolean
// success value, which must be true.
uint256 result = LibBytesV06.readUint256(resultData, 0);
if (result != 1) {
LibRichErrorsV06.rrevert(resultData);
}
} else {
// If 0 < returndatasize < 32, the target is a contract, but not a
// valid token.
LibRichErrorsV06.rrevert(resultData);
}
}
}
}

View File

@ -40,7 +40,7 @@ contract UniswapV3Feature is
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "UniswapV3Feature";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0);
/// @dev WETH contract.
IEtherTokenV06 private immutable WETH;
/// @dev UniswapV3 Factory contract address prepended with '0xff' and left-aligned.
@ -88,6 +88,7 @@ contract UniswapV3Feature is
_registerFeatureFunction(this.sellEthForTokenToUniswapV3.selector);
_registerFeatureFunction(this.sellTokenForEthToUniswapV3.selector);
_registerFeatureFunction(this.sellTokenForTokenToUniswapV3.selector);
_registerFeatureFunction(this._sellHeldTokenForTokenToUniswapV3.selector);
_registerFeatureFunction(this.uniswapV3SwapCallback.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
@ -175,6 +176,33 @@ contract UniswapV3Feature is
);
}
/// @dev Sell a token for another token directly against uniswap v3.
/// Private variant, uses tokens held by `address(this)`.
/// @param encodedPath Uniswap-encoded path.
/// @param sellAmount amount of the first token in the path to sell.
/// @param minBuyAmount Minimum amount of the last token in the path to buy.
/// @param recipient The recipient of the bought tokens. Can be zero for sender.
/// @return buyAmount Amount of the last token in the path bought.
function _sellHeldTokenForTokenToUniswapV3(
bytes memory encodedPath,
uint256 sellAmount,
uint256 minBuyAmount,
address recipient
)
public
override
onlySelf
returns (uint256 buyAmount)
{
buyAmount = _swap(
encodedPath,
sellAmount,
minBuyAmount,
address(this),
_normalizeRecipient(recipient)
);
}
/// @dev The UniswapV3 pool swap callback which pays the funds requested
/// by the caller/pool to the pool. Can only be called by a valid
/// UniswapV3 pool.

View File

@ -24,9 +24,21 @@ import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
interface IMultiplexFeature {
// Identifies the type of subcall.
enum MultiplexSubcall {
Invalid,
RFQ,
OTC,
UniswapV2,
UniswapV3,
LiquidityProvider,
TransformERC20,
BatchSell,
MultiHopSell
}
// Parameters for `batchFill`.
struct BatchFillData {
// Parameters for a batch sell.
struct BatchSellParams {
// The token being sold.
IERC20TokenV06 inputToken;
// The token being bought.
@ -34,84 +46,182 @@ interface IMultiplexFeature {
// The amount of `inputToken` to sell.
uint256 sellAmount;
// The nested calls to perform.
WrappedBatchCall[] calls;
BatchSellSubcall[] calls;
// Whether to use the Exchange Proxy's balance
// of input tokens.
bool useSelfBalance;
// The recipient of the bought output tokens.
address recipient;
}
// Represents a call nested within a `batchFill`.
struct WrappedBatchCall {
// The selector of the function to call.
bytes4 selector;
// Amount of `inputToken` to sell.
// Represents a constituent call of a batch sell.
struct BatchSellSubcall {
// The function to call.
MultiplexSubcall id;
// Amount of input token to sell. If the highest bit is 1,
// this value represents a proportion of the total
// `sellAmount` of the batch sell. See `_normalizeSellAmount`
// for details.
uint256 sellAmount;
// ABI-encoded parameters needed to perform the call.
bytes data;
}
// Parameters for `multiHopFill`.
struct MultiHopFillData {
// Parameters for a multi-hop sell.
struct MultiHopSellParams {
// 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;
MultiHopSellSubcall[] calls;
// Whether to use the Exchange Proxy's balance
// of input tokens.
bool useSelfBalance;
// The recipient of the bought output tokens.
address recipient;
}
// Represents a call nested within a `multiHopFill`.
struct WrappedMultiHopCall {
// The selector of the function to call.
bytes4 selector;
// Represents a constituent call of a multi-hop sell.
struct MultiHopSellSubcall {
// The function to call.
MultiplexSubcall id;
// ABI-encoded parameters needed to perform the call.
bytes data;
}
event LiquidityProviderSwap(
address inputToken,
address outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount,
address provider,
address recipient
);
struct BatchSellState {
// Tracks the amount of input token sold.
uint256 soldAmount;
// Tracks the amount of output token bought.
uint256 boughtAmount;
}
event ExpiredRfqOrder(
bytes32 orderHash,
address maker,
uint64 expiry
);
struct MultiHopSellState {
// This variable is used for the input and output amounts of
// each hop. After the final hop, this will contain the output
// amount of the multi-hop sell.
uint256 outputTokenAmount;
// For each hop in a multi-hop sell, `from` is the
// address that holds the input tokens of the hop,
// `to` is the address that receives the output tokens
// of the hop.
// See `_computeHopTarget` for details.
address from;
address to;
// The index of the current hop in the multi-hop chain.
uint256 hopIndex;
}
/// @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,
/// @dev Sells attached ETH for `outputToken` using the provided
/// calls.
/// @param outputToken The token to buy.
/// @param calls The calls to use to sell the attached ETH.
/// @param minBuyAmount The minimum amount of `outputToken` that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of `outputToken` bought.
function multiplexBatchSellEthForToken(
IERC20TokenV06 outputToken,
BatchSellSubcall[] calldata calls,
uint256 minBuyAmount
)
external
payable
returns (uint256 outputTokenAmount);
returns (uint256 boughtAmount);
/// @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,
/// @dev Sells `sellAmount` of the given `inputToken` for ETH
/// using the provided calls.
/// @param inputToken The token to sell.
/// @param calls The calls to use to sell the input tokens.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of ETH that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of ETH bought.
function multiplexBatchSellTokenForEth(
IERC20TokenV06 inputToken,
BatchSellSubcall[] calldata calls,
uint256 sellAmount,
uint256 minBuyAmount
)
external
returns (uint256 boughtAmount);
/// @dev Sells `sellAmount` of the given `inputToken` for
/// `outputToken` using the provided calls.
/// @param inputToken The token to sell.
/// @param outputToken The token to buy.
/// @param calls The calls to use to sell the input tokens.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of `outputToken`
/// that must be bought for this function to not revert.
/// @return boughtAmount The amount of `outputToken` bought.
function multiplexBatchSellTokenForToken(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
BatchSellSubcall[] calldata calls,
uint256 sellAmount,
uint256 minBuyAmount
)
external
returns (uint256 boughtAmount);
/// @dev Sells attached ETH via the given sequence of tokens
/// and calls. `tokens[0]` must be WETH.
/// The last token in `tokens` is the output token that
/// will ultimately be sent to `msg.sender`
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param minBuyAmount The minimum amount of output tokens that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of output tokens bought.
function multiplexMultiHopSellEthForToken(
address[] calldata tokens,
MultiHopSellSubcall[] calldata calls,
uint256 minBuyAmount
)
external
payable
returns (uint256 outputTokenAmount);
returns (uint256 boughtAmount);
/// @dev Sells `sellAmount` of the input token (`tokens[0]`)
/// for ETH via the given sequence of tokens and calls.
/// The last token in `tokens` must be WETH.
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param minBuyAmount The minimum amount of ETH that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of ETH bought.
function multiplexMultiHopSellTokenForEth(
address[] calldata tokens,
MultiHopSellSubcall[] calldata calls,
uint256 sellAmount,
uint256 minBuyAmount
)
external
returns (uint256 boughtAmount);
/// @dev Sells `sellAmount` of the input token (`tokens[0]`)
/// via the given sequence of tokens and calls.
/// The last token in `tokens` is the output token that
/// will ultimately be sent to `msg.sender`
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param minBuyAmount The minimum amount of output tokens that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of output tokens bought.
function multiplexMultiHopSellTokenForToken(
address[] calldata tokens,
MultiHopSellSubcall[] calldata calls,
uint256 sellAmount,
uint256 minBuyAmount
)
external
returns (uint256 boughtAmount);
}

View File

@ -126,13 +126,18 @@ interface INativeOrdersFeature is
/// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token to fill this order with.
/// @param taker The order taker.
/// @param useSelfBalance Whether to use the ExchangeProxy's transient
/// balance of taker tokens to fill the order.
/// @param recipient The recipient of the maker tokens.
/// @return takerTokenFilledAmount How much maker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillRfqOrder(
LibNativeOrder.RfqOrder calldata order,
LibSignature.Signature calldata signature,
uint128 takerTokenFillAmount,
address taker
address taker,
bool useSelfBalance,
address recipient
)
external
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);

View File

@ -31,16 +31,16 @@ interface IOtcOrdersFeature {
/// @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 takerTokenFilledAmount How much taker token was filled.
event OtcOrderFilled(
bytes32 orderHash,
address maker,
address taker,
address makerToken,
address takerToken,
uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount
uint128 makerTokenFilledAmount,
uint128 takerTokenFilledAmount
);
/// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens.
@ -48,15 +48,29 @@ interface IOtcOrdersFeature {
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @param unwrapWeth Whether or not to unwrap bought WETH into ETH
/// before transferring it to the taker. Should be set to false
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOtcOrder(
LibNativeOrder.OtcOrder calldata order,
LibSignature.Signature calldata makerSignature,
uint128 takerTokenFillAmount,
bool unwrapWeth
uint128 takerTokenFillAmount
)
external
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
/// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens.
/// Unwraps bought WETH into ETH before sending it to
/// the taker.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillOtcOrderForEth(
LibNativeOrder.OtcOrder calldata order,
LibSignature.Signature calldata makerSignature,
uint128 takerTokenFillAmount
)
external
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
@ -80,16 +94,64 @@ interface IOtcOrdersFeature {
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerSignature The order signature from the taker.
/// @param unwrapWeth Whether or not to unwrap bought WETH into ETH
/// before transferring it to the taker. Should be set to false
/// if the maker token is not WETH.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function fillTakerSignedOtcOrder(
LibNativeOrder.OtcOrder calldata order,
LibSignature.Signature calldata makerSignature,
LibSignature.Signature calldata takerSignature,
bool unwrapWeth
LibSignature.Signature calldata takerSignature
)
external;
/// @dev Fully fill an OTC order. "Meta-transaction" variant,
/// requires order to be signed by both maker and taker.
/// Unwraps bought WETH into ETH before sending it to
/// the taker.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerSignature The order signature from the taker.
function fillTakerSignedOtcOrderForEth(
LibNativeOrder.OtcOrder calldata order,
LibSignature.Signature calldata makerSignature,
LibSignature.Signature calldata takerSignature
)
external;
/// @dev Fills multiple taker-signed OTC orders.
/// @param orders Array of OTC orders.
/// @param makerSignatures Array of maker signatures for each order.
/// @param takerSignatures Array of taker signatures for each order.
/// @param unwrapWeth Array of booleans representing whether or not
/// to unwrap bought WETH into ETH for each order. Should be set
/// to false if the maker token is not WETH.
/// @return successes Array of booleans representing whether or not
/// each order in `orders` was filled successfully.
function batchFillTakerSignedOtcOrders(
LibNativeOrder.OtcOrder[] calldata orders,
LibSignature.Signature[] calldata makerSignatures,
LibSignature.Signature[] calldata takerSignatures,
bool[] calldata unwrapWeth
)
external
returns (bool[] memory successes);
/// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens.
/// Internal variant.
/// @param order The OTC order.
/// @param makerSignature The order signature from the maker.
/// @param takerTokenFillAmount Maximum taker token amount to fill this
/// order with.
/// @param taker The address to fill the order in the context of.
/// @param useSelfBalance Whether to use the Exchange Proxy's balance
/// of input tokens.
/// @param recipient The recipient of the bought maker tokens.
/// @return takerTokenFilledAmount How much taker token was filled.
/// @return makerTokenFilledAmount How much maker token was filled.
function _fillOtcOrder(
LibNativeOrder.OtcOrder calldata order,
LibSignature.Signature calldata makerSignature,
uint128 takerTokenFillAmount,
address taker,
bool useSelfBalance,
address recipient
)
external
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);

View File

@ -59,6 +59,10 @@ interface ITransformERC20Feature {
// The transformations to execute on the token balance(s)
// in sequence.
Transformation[] transformations;
// Whether to use the Exchange Proxy's balance of `inputToken`.
bool useSelfBalance;
// The recipient of the bought `outputToken`.
address payable recipient;
}
/// @dev Raised upon a successful `transformERC20`.

View File

@ -70,6 +70,22 @@ interface IUniswapV3Feature {
external
returns (uint256 buyAmount);
/// @dev Sell a token for another token directly against uniswap v3.
/// Private variant, uses tokens held by `address(this)`.
/// @param encodedPath Uniswap-encoded path.
/// @param sellAmount amount of the first token in the path to sell.
/// @param minBuyAmount Minimum amount of the last token in the path to buy.
/// @param recipient The recipient of the bought tokens. Can be zero for sender.
/// @return buyAmount Amount of the last token in the path bought.
function _sellHeldTokenForTokenToUniswapV3(
bytes memory encodedPath,
uint256 sellAmount,
uint256 minBuyAmount,
address recipient
)
external
returns (uint256 buyAmount);
/// @dev The UniswapV3 pool swap callback which pays the funds requested
/// by the caller/pool to the pool. Can only be called by a valid
/// UniswapV3 pool.

View File

@ -0,0 +1,742 @@
// 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/LibSafeMathV06.sol";
import "../../external/ILiquidityProviderSandbox.sol";
import "../../fixins/FixinCommon.sol";
import "../../fixins/FixinEIP712.sol";
import "../../migrations/LibMigrate.sol";
import "../interfaces/IFeature.sol";
import "../interfaces/IMultiplexFeature.sol";
import "./MultiplexLiquidityProvider.sol";
import "./MultiplexOtc.sol";
import "./MultiplexRfq.sol";
import "./MultiplexTransformERC20.sol";
import "./MultiplexUniswapV2.sol";
import "./MultiplexUniswapV3.sol";
/// @dev This feature enables efficient batch and multi-hop trades
/// using different liquidity sources.
contract MultiplexFeature is
IFeature,
IMultiplexFeature,
FixinCommon,
MultiplexLiquidityProvider,
MultiplexOtc,
MultiplexRfq,
MultiplexTransformERC20,
MultiplexUniswapV2,
MultiplexUniswapV3
{
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "MultiplexFeature";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(2, 0, 0);
/// @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.
uint256 private constant LOWER_255_BITS = HIGH_BIT - 1;
/// @dev The WETH token contract.
IEtherTokenV06 private immutable WETH;
constructor(
address zeroExAddress,
IEtherTokenV06 weth,
ILiquidityProviderSandbox sandbox,
address uniswapFactory,
address sushiswapFactory,
bytes32 uniswapPairInitCodeHash,
bytes32 sushiswapPairInitCodeHash
)
public
FixinEIP712(zeroExAddress)
MultiplexLiquidityProvider(sandbox)
MultiplexUniswapV2(
uniswapFactory,
sushiswapFactory,
uniswapPairInitCodeHash,
sushiswapPairInitCodeHash
)
{
WETH = weth;
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate()
external
returns (bytes4 success)
{
_registerFeatureFunction(this.multiplexBatchSellEthForToken.selector);
_registerFeatureFunction(this.multiplexBatchSellTokenForEth.selector);
_registerFeatureFunction(this.multiplexBatchSellTokenForToken.selector);
_registerFeatureFunction(this.multiplexMultiHopSellEthForToken.selector);
_registerFeatureFunction(this.multiplexMultiHopSellTokenForEth.selector);
_registerFeatureFunction(this.multiplexMultiHopSellTokenForToken.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Sells attached ETH for `outputToken` using the provided
/// calls.
/// @param outputToken The token to buy.
/// @param calls The calls to use to sell the attached ETH.
/// @param minBuyAmount The minimum amount of `outputToken` that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of `outputToken` bought.
function multiplexBatchSellEthForToken(
IERC20TokenV06 outputToken,
BatchSellSubcall[] memory calls,
uint256 minBuyAmount
)
public
override
payable
returns (uint256 boughtAmount)
{
// Wrap ETH.
WETH.deposit{value: msg.value}();
// WETH is now held by this contract,
// so `useSelfBalance` is true.
return _multiplexBatchSell(
BatchSellParams({
inputToken: WETH,
outputToken: outputToken,
sellAmount: msg.value,
calls: calls,
useSelfBalance: true,
recipient: msg.sender
}),
minBuyAmount
);
}
/// @dev Sells `sellAmount` of the given `inputToken` for ETH
/// using the provided calls.
/// @param inputToken The token to sell.
/// @param calls The calls to use to sell the input tokens.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of ETH that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of ETH bought.
function multiplexBatchSellTokenForEth(
IERC20TokenV06 inputToken,
BatchSellSubcall[] memory calls,
uint256 sellAmount,
uint256 minBuyAmount
)
public
override
returns (uint256 boughtAmount)
{
// The outputToken is implicitly WETH. The `recipient`
// of the WETH is set to this contract, since we
// must unwrap the WETH and transfer the resulting ETH.
boughtAmount = _multiplexBatchSell(
BatchSellParams({
inputToken: inputToken,
outputToken: WETH,
sellAmount: sellAmount,
calls: calls,
useSelfBalance: false,
recipient: address(this)
}),
minBuyAmount
);
// Unwrap WETH.
WETH.withdraw(boughtAmount);
// Transfer ETH to `msg.sender`.
_transferEth(msg.sender, boughtAmount);
}
/// @dev Sells `sellAmount` of the given `inputToken` for
/// `outputToken` using the provided calls.
/// @param inputToken The token to sell.
/// @param outputToken The token to buy.
/// @param calls The calls to use to sell the input tokens.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of `outputToken`
/// that must be bought for this function to not revert.
/// @return boughtAmount The amount of `outputToken` bought.
function multiplexBatchSellTokenForToken(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
BatchSellSubcall[] memory calls,
uint256 sellAmount,
uint256 minBuyAmount
)
public
override
returns (uint256 boughtAmount)
{
return _multiplexBatchSell(
BatchSellParams({
inputToken: inputToken,
outputToken: outputToken,
sellAmount: sellAmount,
calls: calls,
useSelfBalance: false,
recipient: msg.sender
}),
minBuyAmount
);
}
/// @dev Executes a batch sell and checks that at least
/// `minBuyAmount` of `outputToken` was bought.
/// @param params Batch sell parameters.
/// @param minBuyAmount The minimum amount of `outputToken` that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of `outputToken` bought.
function _multiplexBatchSell(
BatchSellParams memory params,
uint256 minBuyAmount
)
private
returns (uint256 boughtAmount)
{
// Cache the recipient's initial balance of the output token.
uint256 balanceBefore = params.outputToken.balanceOf(params.recipient);
// Execute the batch sell.
BatchSellState memory state = _executeBatchSell(params);
// Compute the change in balance of the output token.
uint256 balanceDelta = params.outputToken.balanceOf(params.recipient)
.safeSub(balanceBefore);
// Use the minimum of the balanceDelta and the returned bought
// amount in case of weird tokens and whatnot.
boughtAmount = LibSafeMathV06.min256(balanceDelta, state.boughtAmount);
// Enforce `minBuyAmount`.
require(
boughtAmount >= minBuyAmount,
"MultiplexFeature::_multiplexBatchSell/UNDERBOUGHT"
);
}
/// @dev Sells attached ETH via the given sequence of tokens
/// and calls. `tokens[0]` must be WETH.
/// The last token in `tokens` is the output token that
/// will ultimately be sent to `msg.sender`
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param minBuyAmount The minimum amount of output tokens that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of output tokens bought.
function multiplexMultiHopSellEthForToken(
address[] memory tokens,
MultiHopSellSubcall[] memory calls,
uint256 minBuyAmount
)
public
override
payable
returns (uint256 boughtAmount)
{
// First token must be WETH.
require(
tokens[0] == address(WETH),
"MultiplexFeature::multiplexMultiHopSellEthForToken/NOT_WETH"
);
// Wrap ETH.
WETH.deposit{value: msg.value}();
// WETH is now held by this contract,
// so `useSelfBalance` is true.
return _multiplexMultiHopSell(
MultiHopSellParams({
tokens: tokens,
sellAmount: msg.value,
calls: calls,
useSelfBalance: true,
recipient: msg.sender
}),
minBuyAmount
);
}
/// @dev Sells `sellAmount` of the input token (`tokens[0]`)
/// for ETH via the given sequence of tokens and calls.
/// The last token in `tokens` must be WETH.
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of ETH that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of ETH bought.
function multiplexMultiHopSellTokenForEth(
address[] memory tokens,
MultiHopSellSubcall[] memory calls,
uint256 sellAmount,
uint256 minBuyAmount
)
public
override
returns (uint256 boughtAmount)
{
// Last token must be WETH.
require(
tokens[tokens.length - 1] == address(WETH),
"MultiplexFeature::multiplexMultiHopSellTokenForEth/NOT_WETH"
);
// The `recipient of the WETH is set to this contract, since
// we must unwrap the WETH and transfer the resulting ETH.
boughtAmount = _multiplexMultiHopSell(
MultiHopSellParams({
tokens: tokens,
sellAmount: sellAmount,
calls: calls,
useSelfBalance: false,
recipient: address(this)
}),
minBuyAmount
);
// Unwrap WETH.
WETH.withdraw(boughtAmount);
// Transfer ETH to `msg.sender`.
_transferEth(msg.sender, boughtAmount);
}
/// @dev Sells `sellAmount` of the input token (`tokens[0]`)
/// via the given sequence of tokens and calls.
/// The last token in `tokens` is the output token that
/// will ultimately be sent to `msg.sender`
/// @param tokens The sequence of tokens to use for the sell,
/// i.e. `tokens[i]` will be sold for `tokens[i+1]` via
/// `calls[i]`.
/// @param calls The sequence of calls to use for the sell.
/// @param sellAmount The amount of `inputToken` to sell.
/// @param minBuyAmount The minimum amount of output tokens that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of output tokens bought.
function multiplexMultiHopSellTokenForToken(
address[] memory tokens,
MultiHopSellSubcall[] memory calls,
uint256 sellAmount,
uint256 minBuyAmount
)
public
override
returns (uint256 boughtAmount)
{
return _multiplexMultiHopSell(
MultiHopSellParams({
tokens: tokens,
sellAmount: sellAmount,
calls: calls,
useSelfBalance: false,
recipient: msg.sender
}),
minBuyAmount
);
}
/// @dev Executes a multi-hop sell and checks that at least
/// `minBuyAmount` of output tokens were bought.
/// @param params Multi-hop sell parameters.
/// @param minBuyAmount The minimum amount of output tokens that
/// must be bought for this function to not revert.
/// @return boughtAmount The amount of output tokens bought.
function _multiplexMultiHopSell(
MultiHopSellParams memory params,
uint256 minBuyAmount
)
private
returns (uint256 boughtAmount)
{
// 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(
params.tokens.length == params.calls.length + 1,
"MultiplexFeature::_multiplexMultiHopSell/MISMATCHED_ARRAY_LENGTHS"
);
// The output token is the last token in the path.
IERC20TokenV06 outputToken = IERC20TokenV06(
params.tokens[params.tokens.length - 1]
);
// Cache the recipient's balance of the output token.
uint256 balanceBefore = outputToken.balanceOf(params.recipient);
// Execute the multi-hop sell.
MultiHopSellState memory state = _executeMultiHopSell(params);
// Compute the change in balance of the output token.
uint256 balanceDelta = outputToken.balanceOf(params.recipient)
.safeSub(balanceBefore);
// Use the minimum of the balanceDelta and the returned bought
// amount in case of weird tokens and whatnot.
boughtAmount = LibSafeMathV06.min256(balanceDelta, state.outputTokenAmount);
// Enforce `minBuyAmount`.
require(
boughtAmount >= minBuyAmount,
"MultiplexFeature::_multiplexMultiHopSell/UNDERBOUGHT"
);
}
/// @dev Iterates through the constituent calls of a batch
/// sell and executes each one, until the full amount
// has been sold.
/// @param params Batch sell parameters.
/// @return state A struct containing the amounts of `inputToken`
/// sold and `outputToken` bought.
function _executeBatchSell(BatchSellParams memory params)
private
returns (BatchSellState memory state)
{
// Iterate through the calls and execute each one
// until the full amount has been sold.
for (uint256 i = 0; i != params.calls.length; i++) {
// Check if we've hit our target.
if (state.soldAmount >= params.sellAmount) { break; }
BatchSellSubcall memory subcall = params.calls[i];
// Compute the input token amount.
uint256 inputTokenAmount = _normalizeSellAmount(
subcall.sellAmount,
params.sellAmount,
state.soldAmount
);
if (subcall.id == MultiplexSubcall.RFQ) {
_batchSellRfqOrder(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.OTC) {
_batchSellOtcOrder(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.UniswapV2) {
_batchSellUniswapV2(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.UniswapV3) {
_batchSellUniswapV3(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.LiquidityProvider) {
_batchSellLiquidityProvider(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.TransformERC20) {
_batchSellTransformERC20(
state,
params,
subcall.data,
inputTokenAmount
);
} else if (subcall.id == MultiplexSubcall.MultiHopSell) {
_nestedMultiHopSell(
state,
params,
subcall.data,
inputTokenAmount
);
} else {
revert("MultiplexFeature::_executeBatchSell/INVALID_SUBCALL");
}
}
require(
state.soldAmount == params.sellAmount,
"MultiplexFeature::_executeBatchSell/INCORRECT_AMOUNT_SOLD"
);
}
// This function executes a sequence of fills "hopping" through the
// path of tokens given by `params.tokens`.
function _executeMultiHopSell(MultiHopSellParams memory params)
private
returns (MultiHopSellState memory state)
{
// This variable is used for the input and output amounts of
// each hop. After the final hop, this will contain the output
// amount of the multi-hop fill.
state.outputTokenAmount = params.sellAmount;
// The first call may expect the input tokens to be held by
// `msg.sender`, `address(this)`, or some other address.
// Compute the expected address and transfer the input tokens
// there if necessary.
state.from = _computeHopTarget(params, 0);
// If the input tokens are currently held by `msg.sender` but
// the first hop expects them elsewhere, perform a `transferFrom`.
if (!params.useSelfBalance && state.from != msg.sender) {
_transferERC20TokensFrom(
IERC20TokenV06(params.tokens[0]),
msg.sender,
state.from,
params.sellAmount
);
}
// If the input tokens are currently held by `address(this)` but
// the first hop expects them elsewhere, perform a `transfer`.
if (params.useSelfBalance && state.from != address(this)) {
_transferERC20Tokens(
IERC20TokenV06(params.tokens[0]),
state.from,
params.sellAmount
);
}
// Iterate through the calls and execute each one.
for (state.hopIndex = 0; state.hopIndex != params.calls.length; state.hopIndex++) {
MultiHopSellSubcall memory subcall = params.calls[state.hopIndex];
// Compute the recipient of the tokens that will be
// bought by the current hop.
state.to = _computeHopTarget(params, state.hopIndex + 1);
if (subcall.id == MultiplexSubcall.UniswapV2) {
_multiHopSellUniswapV2(
state,
params,
subcall.data
);
} else if (subcall.id == MultiplexSubcall.UniswapV3) {
_multiHopSellUniswapV3(state, subcall.data);
} else if (subcall.id == MultiplexSubcall.LiquidityProvider) {
_multiHopSellLiquidityProvider(
state,
params,
subcall.data
);
} else if (subcall.id == MultiplexSubcall.BatchSell) {
_nestedBatchSell(
state,
params,
subcall.data
);
} else {
revert("MultiplexFeature::_executeMultiHopSell/INVALID_SUBCALL");
}
// The recipient of the current hop will be the source
// of tokens for the next hop.
state.from = state.to;
}
}
function _nestedMultiHopSell(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory data,
uint256 sellAmount
)
private
{
MultiHopSellParams memory multiHopParams;
// Decode the tokens and calls for the nested
// multi-hop sell.
(
multiHopParams.tokens,
multiHopParams.calls
) = abi.decode(
data,
(address[], MultiHopSellSubcall[])
);
multiHopParams.sellAmount = sellAmount;
// If the batch sell is using input tokens held by
// `address(this)`, then so should the nested
// multi-hop sell.
multiHopParams.useSelfBalance = params.useSelfBalance;
// Likewise, the recipient of the multi-hop sell is
// equal to the recipient of its containing batch sell.
multiHopParams.recipient = params.recipient;
// Execute the nested multi-hop sell.
uint256 outputTokenAmount =
_executeMultiHopSell(multiHopParams).outputTokenAmount;
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(sellAmount);
state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount);
}
function _nestedBatchSell(
IMultiplexFeature.MultiHopSellState memory state,
IMultiplexFeature.MultiHopSellParams memory params,
bytes memory data
)
private
{
BatchSellParams memory batchSellParams;
// Decode the calls for the nested batch sell.
batchSellParams.calls = abi.decode(
data,
(BatchSellSubcall[])
);
// The input and output tokens of the batch
// sell are the current and next tokens in
// `params.tokens`, respectively.
batchSellParams.inputToken = IERC20TokenV06(
params.tokens[state.hopIndex]
);
batchSellParams.outputToken = IERC20TokenV06(
params.tokens[state.hopIndex + 1]
);
// The `sellAmount` for the batch sell is the
// `outputTokenAmount` from the previous hop.
batchSellParams.sellAmount = state.outputTokenAmount;
// If the nested batch sell is the first hop
// and `useSelfBalance` for the containing multi-
// hop sell is false, the nested batch sell should
// pull tokens from `msg.sender` (so `batchSellParams.useSelfBalance`
// should be false). Otherwise `batchSellParams.useSelfBalance`
// should be true.
batchSellParams.useSelfBalance = state.hopIndex > 0 || params.useSelfBalance;
// `state.to` has been populated with the address
// that should receive the output tokens of the
// batch sell.
batchSellParams.recipient = state.to;
// Execute the nested batch sell.
state.outputTokenAmount =
_executeBatchSell(batchSellParams).boughtAmount;
}
// Transfers some amount of ETH to the given recipient and
// reverts if the transfer fails.
function _transferEth(address payable recipient, uint256 amount)
private
{
(bool success,) = recipient.call{value: amount}("");
require(success, "MultiplexFeature::_transferEth/TRANSFER_FAILED");
}
// This function computes the "target" address of hop index `i` within
// a multi-hop sell.
// If `i == 0`, the target is the address which should hold the input
// tokens prior to executing `calls[0]`. Otherwise, it is the address
// that should receive `tokens[i]` upon executing `calls[i-1]`.
function _computeHopTarget(
MultiHopSellParams memory params,
uint256 i
)
private
view
returns (address target)
{
if (i == params.calls.length) {
// The last call should send the output tokens to the
// multi-hop sell recipient.
target = params.recipient;
} else {
MultiHopSellSubcall memory subcall = params.calls[i];
if (subcall.id == MultiplexSubcall.UniswapV2) {
// UniswapV2 (and Sushiswap) allow tokens to be
// transferred into the pair contract before `swap`
// is called, so we compute the pair contract's address.
(address[] memory tokens, bool isSushi) = abi.decode(
subcall.data,
(address[], bool)
);
target = _computeUniswapPairAddress(
tokens[0],
tokens[1],
isSushi
);
} else if (subcall.id == MultiplexSubcall.LiquidityProvider) {
// Similar to UniswapV2, LiquidityProvider contracts
// allow tokens to be transferred in before the swap
// is executed, so we the target is the address encoded
// in the subcall data.
(target,) = abi.decode(
subcall.data,
(address, bytes)
);
} else if (
subcall.id == MultiplexSubcall.UniswapV3 ||
subcall.id == MultiplexSubcall.BatchSell
) {
// UniswapV3 uses a callback to pull in the tokens being
// sold to it. The callback implemented in `UniswapV3Feature`
// can either:
// - call `transferFrom` to move tokens from `msg.sender` to the
// UniswapV3 pool, or
// - call `transfer` to move tokens from `address(this)` to the
// UniswapV3 pool.
// A nested batch sell is similar, in that it can either:
// - use tokens from `msg.sender`, or
// - use tokens held by `address(this)`.
// Suppose UniswapV3/BatchSell is the first call in the multi-hop
// path. The input tokens are either held by `msg.sender`,
// or in the case of `multiplexMultiHopSellEthForToken` WETH is
// held by `address(this)`. The target is set accordingly.
// If this is _not_ the first call in the multi-hop path, we
// are dealing with an "intermediate" token in the multi-hop path,
// which `msg.sender` may not have an allowance set for. Thus
// target must be set to `address(this)` for `i > 0`.
if (i == 0 && !params.useSelfBalance) {
target = msg.sender;
} else {
target = address(this);
}
} else {
revert("MultiplexFeature::_computeHopTarget/INVALID_SUBCALL");
}
}
require(
target != address(0),
"MultiplexFeature::_computeHopTarget/TARGET_IS_NULL"
);
}
// If `rawAmount` encodes a proportion of `totalSellAmount`, this function
// converts it to an absolute quantity. Caps the normalized amount to
// the remaining sell amount (`totalSellAmount - soldAmount`).
function _normalizeSellAmount(
uint256 rawAmount,
uint256 totalSellAmount,
uint256 soldAmount
)
private
pure
returns (uint256 normalized)
{
if ((rawAmount & HIGH_BIT) == HIGH_BIT) {
// If the high bit of `rawAmount` is set then the lower 255 bits
// specify a fraction of `totalSellAmount`.
return LibSafeMathV06.min256(
totalSellAmount
* LibSafeMathV06.min256(rawAmount & LOWER_255_BITS, 1e18)
/ 1e18,
totalSellAmount.safeSub(soldAmount)
);
} else {
return LibSafeMathV06.min256(
rawAmount,
totalSellAmount.safeSub(soldAmount)
);
}
}
}

View File

@ -0,0 +1,202 @@
// 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/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../../external/ILiquidityProviderSandbox.sol";
import "../../fixins/FixinCommon.sol";
import "../../fixins/FixinTokenSpender.sol";
import "../../vendor/ILiquidityProvider.sol";
import "../interfaces/IMultiplexFeature.sol";
abstract contract MultiplexLiquidityProvider is
FixinCommon,
FixinTokenSpender
{
using LibERC20TokenV06 for IERC20TokenV06;
using LibSafeMathV06 for uint256;
// Same event fired by LiquidityProviderFeature
event LiquidityProviderSwap(
address inputToken,
address outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount,
address provider,
address recipient
);
/// @dev The sandbox contract address.
ILiquidityProviderSandbox private immutable SANDBOX;
constructor(ILiquidityProviderSandbox sandbox)
internal
{
SANDBOX = sandbox;
}
// A payable external function that we can delegatecall to
// swallow reverts and roll back the input token transfer.
function _batchSellLiquidityProviderExternal(
IMultiplexFeature.BatchSellParams calldata params,
bytes calldata wrappedCallData,
uint256 sellAmount
)
external
payable
returns (uint256 boughtAmount)
{
// Revert if not a delegatecall.
require(
address(this) != _implementation,
"MultiplexLiquidityProvider::_batchSellLiquidityProviderExternal/ONLY_DELEGATECALL"
);
// Decode the provider address and auxiliary data.
(address provider, bytes memory auxiliaryData) = abi.decode(
wrappedCallData,
(address, bytes)
);
if (params.useSelfBalance) {
// If `useSelfBalance` is true, use the input tokens
// held by `address(this)`.
_transferERC20Tokens(
params.inputToken,
provider,
sellAmount
);
} else {
// Otherwise, transfer the input tokens from `msg.sender`.
_transferERC20TokensFrom(
params.inputToken,
msg.sender,
provider,
sellAmount
);
}
// Cache the recipient's balance of the output token.
uint256 balanceBefore = params.outputToken
.balanceOf(params.recipient);
// Execute the swap.
SANDBOX.executeSellTokenForToken(
ILiquidityProvider(provider),
params.inputToken,
params.outputToken,
params.recipient,
0,
auxiliaryData
);
// Compute amount of output token received by the
// recipient.
boughtAmount = params.outputToken
.balanceOf(params.recipient)
.safeSub(balanceBefore);
emit LiquidityProviderSwap(
address(params.inputToken),
address(params.outputToken),
sellAmount,
boughtAmount,
provider,
params.recipient
);
}
function _batchSellLiquidityProvider(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
// Swallow reverts
(bool success, bytes memory resultData) = _implementation.delegatecall(
abi.encodeWithSelector(
this._batchSellLiquidityProviderExternal.selector,
params,
wrappedCallData,
sellAmount
)
);
if (success) {
// Decode the output token amount on success.
uint256 boughtAmount = abi.decode(resultData, (uint256));
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(sellAmount);
state.boughtAmount = state.boughtAmount.safeAdd(boughtAmount);
}
}
// This function is called after tokens have already been transferred
// into the liquidity provider contract (in the previous hop).
function _multiHopSellLiquidityProvider(
IMultiplexFeature.MultiHopSellState memory state,
IMultiplexFeature.MultiHopSellParams memory params,
bytes memory wrappedCallData
)
internal
{
IERC20TokenV06 inputToken = IERC20TokenV06(params.tokens[state.hopIndex]);
IERC20TokenV06 outputToken = IERC20TokenV06(params.tokens[state.hopIndex + 1]);
// Decode the provider address and auxiliary data.
(address provider, bytes memory auxiliaryData) = abi.decode(
wrappedCallData,
(address, bytes)
);
// Cache the recipient's balance of the output token.
uint256 balanceBefore = outputToken
.balanceOf(state.to);
// Execute the swap.
SANDBOX.executeSellTokenForToken(
ILiquidityProvider(provider),
inputToken,
outputToken,
state.to,
0,
auxiliaryData
);
// The previous `ouputTokenAmount` was effectively the
// input amount for this call. Cache the value before
// overwriting it with the new output token amount so
// that both the input and ouput amounts can be in the
// `LiquidityProviderSwap` event.
uint256 sellAmount = state.outputTokenAmount;
// Compute amount of output token received by the
// recipient.
state.outputTokenAmount = outputToken
.balanceOf(state.to)
.safeSub(balanceBefore);
emit LiquidityProviderSwap(
address(inputToken),
address(outputToken),
sellAmount,
state.outputTokenAmount,
provider,
state.to
);
}
}

View File

@ -0,0 +1,94 @@
// 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/LibSafeMathV06.sol";
import "../../fixins/FixinEIP712.sol";
import "../interfaces/IMultiplexFeature.sol";
import "../interfaces/IOtcOrdersFeature.sol";
import "../libs/LibNativeOrder.sol";
abstract contract MultiplexOtc is
FixinEIP712
{
using LibSafeMathV06 for uint256;
event ExpiredOtcOrder(
bytes32 orderHash,
address maker,
uint64 expiry
);
function _batchSellOtcOrder(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
// Decode the Otc order and signature.
(
LibNativeOrder.OtcOrder memory order,
LibSignature.Signature memory signature
) = abi.decode(
wrappedCallData,
(LibNativeOrder.OtcOrder, LibSignature.Signature)
);
// Validate tokens.
require(
order.takerToken == params.inputToken &&
order.makerToken == params.outputToken,
"MultiplexOtc::_batchSellOtcOrder/OTC_ORDER_INVALID_TOKENS"
);
// Pre-emptively check if the order is expired.
uint64 expiry = uint64(order.expiryAndNonce >> 192);
if (expiry <= uint64(block.timestamp)) {
bytes32 orderHash = _getEIP712Hash(
LibNativeOrder.getOtcOrderStructHash(order)
);
emit ExpiredOtcOrder(
orderHash,
order.maker,
expiry
);
return;
}
// Try filling the Otc order. Swallows reverts.
try
IOtcOrdersFeature(address(this))._fillOtcOrder
(
order,
signature,
sellAmount.safeDowncastToUint128(),
msg.sender,
params.useSelfBalance,
params.recipient
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(takerTokenFilledAmount);
state.boughtAmount = state.boughtAmount.safeAdd(makerTokenFilledAmount);
} catch {}
}
}

View File

@ -0,0 +1,93 @@
// 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/LibSafeMathV06.sol";
import "../../fixins/FixinEIP712.sol";
import "../interfaces/IMultiplexFeature.sol";
import "../interfaces/INativeOrdersFeature.sol";
import "../libs/LibNativeOrder.sol";
abstract contract MultiplexRfq is
FixinEIP712
{
using LibSafeMathV06 for uint256;
event ExpiredRfqOrder(
bytes32 orderHash,
address maker,
uint64 expiry
);
function _batchSellRfqOrder(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
// Decode the RFQ order and signature.
(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature
) = abi.decode(
wrappedCallData,
(LibNativeOrder.RfqOrder, LibSignature.Signature)
);
// Pre-emptively check if the order is expired.
if (order.expiry <= uint64(block.timestamp)) {
bytes32 orderHash = _getEIP712Hash(
LibNativeOrder.getRfqOrderStructHash(order)
);
emit ExpiredRfqOrder(
orderHash,
order.maker,
order.expiry
);
return;
}
// Validate tokens.
require(
order.takerToken == params.inputToken &&
order.makerToken == params.outputToken,
"MultiplexRfq::_batchSellRfqOrder/RFQ_ORDER_INVALID_TOKENS"
);
// Try filling the RFQ order. Swallows reverts.
try
INativeOrdersFeature(address(this))._fillRfqOrder
(
order,
signature,
sellAmount.safeDowncastToUint128(),
msg.sender,
params.useSelfBalance,
params.recipient
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(takerTokenFilledAmount);
state.boughtAmount = state.boughtAmount.safeAdd(makerTokenFilledAmount);
} catch {}
}
}

View File

@ -0,0 +1,64 @@
// 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/LibSafeMathV06.sol";
import "../interfaces/IMultiplexFeature.sol";
import "../interfaces/ITransformERC20Feature.sol";
abstract contract MultiplexTransformERC20 {
using LibSafeMathV06 for uint256;
function _batchSellTransformERC20(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
ITransformERC20Feature.TransformERC20Args memory args;
// We want the TransformedERC20 event to have
// `msg.sender` as the taker.
args.taker = msg.sender;
args.inputToken = params.inputToken;
args.outputToken = params.outputToken;
args.inputTokenAmount = sellAmount;
args.minOutputTokenAmount = 0;
args.useSelfBalance = params.useSelfBalance;
args.recipient = payable(params.recipient);
(args.transformations) = abi.decode(
wrappedCallData,
(ITransformERC20Feature.Transformation[])
);
// Execute the transformations and swallow reverts.
try ITransformERC20Feature(address(this))._transformERC20
(args)
returns (uint256 outputTokenAmount)
{
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(sellAmount);
state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount);
} catch {}
}
}

View File

@ -0,0 +1,290 @@
// 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 "../../fixins/FixinCommon.sol";
import "../../fixins/FixinTokenSpender.sol";
import "../../vendor/IUniswapV2Pair.sol";
import "../interfaces/IMultiplexFeature.sol";
abstract contract MultiplexUniswapV2 is
FixinCommon,
FixinTokenSpender
{
using LibSafeMathV06 for uint256;
// address of the UniswapV2Factory contract.
address private immutable UNISWAP_FACTORY;
// address of the (Sushiswap) UniswapV2Factory contract.
address private immutable SUSHISWAP_FACTORY;
// Init code hash of the UniswapV2Pair contract.
bytes32 private immutable UNISWAP_PAIR_INIT_CODE_HASH;
// Init code hash of the (Sushiswap) UniswapV2Pair contract.
bytes32 private immutable SUSHISWAP_PAIR_INIT_CODE_HASH;
constructor(
address uniswapFactory,
address sushiswapFactory,
bytes32 uniswapPairInitCodeHash,
bytes32 sushiswapPairInitCodeHash
)
internal
{
UNISWAP_FACTORY = uniswapFactory;
SUSHISWAP_FACTORY = sushiswapFactory;
UNISWAP_PAIR_INIT_CODE_HASH = uniswapPairInitCodeHash;
SUSHISWAP_PAIR_INIT_CODE_HASH = sushiswapPairInitCodeHash;
}
// A payable external function that we can delegatecall to
// swallow reverts and roll back the input token transfer.
function _batchSellUniswapV2External(
IMultiplexFeature.BatchSellParams calldata params,
bytes calldata wrappedCallData,
uint256 sellAmount
)
external
payable
returns (uint256 boughtAmount)
{
// Revert is not a delegatecall.
require(
address(this) != _implementation,
"MultiplexLiquidityProvider::_batchSellUniswapV2External/ONLY_DELEGATECALL"
);
(address[] memory tokens, bool isSushi) = abi.decode(
wrappedCallData,
(address[], bool)
);
// Validate tokens
require(
tokens.length >= 2 &&
tokens[0] == address(params.inputToken) &&
tokens[tokens.length - 1] == address(params.outputToken),
"MultiplexUniswapV2::_batchSellUniswapV2/INVALID_TOKENS"
);
// Compute the address of the first Uniswap pair
// contract that will execute a swap.
address firstPairAddress = _computeUniswapPairAddress(
tokens[0],
tokens[1],
isSushi
);
// `_sellToUniswapV2` assumes the input tokens have been
// transferred into the pair contract before it is called,
// so we transfer the tokens in now (either from `msg.sender`
// or using the Exchange Proxy's balance).
if (params.useSelfBalance) {
_transferERC20Tokens(
IERC20TokenV06(tokens[0]),
firstPairAddress,
sellAmount
);
} else {
_transferERC20TokensFrom(
IERC20TokenV06(tokens[0]),
msg.sender,
firstPairAddress,
sellAmount
);
}
// Execute the Uniswap/Sushiswap trade.
return _sellToUniswapV2(
tokens,
sellAmount,
isSushi,
firstPairAddress,
params.recipient
);
}
function _batchSellUniswapV2(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
// Swallow reverts
(bool success, bytes memory resultData) = _implementation.delegatecall(
abi.encodeWithSelector(
this._batchSellUniswapV2External.selector,
params,
wrappedCallData,
sellAmount
)
);
if (success) {
// Decode the output token amount on success.
uint256 boughtAmount = abi.decode(resultData, (uint256));
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(sellAmount);
state.boughtAmount = state.boughtAmount.safeAdd(boughtAmount);
}
}
function _multiHopSellUniswapV2(
IMultiplexFeature.MultiHopSellState memory state,
IMultiplexFeature.MultiHopSellParams memory params,
bytes memory wrappedCallData
)
internal
{
(address[] memory tokens, bool isSushi) = abi.decode(
wrappedCallData,
(address[], bool)
);
// Validate the tokens
require(
tokens.length >= 2 &&
tokens[0] == params.tokens[state.hopIndex] &&
tokens[tokens.length - 1] == params.tokens[state.hopIndex + 1],
"MultiplexUniswapV2::_multiHopSellUniswapV2/INVALID_TOKENS"
);
// Execute the Uniswap/Sushiswap trade.
state.outputTokenAmount = _sellToUniswapV2(
tokens,
state.outputTokenAmount,
isSushi,
state.from,
state.to
);
}
function _sellToUniswapV2(
address[] memory tokens,
uint256 sellAmount,
bool isSushi,
address pairAddress,
address recipient
)
private
returns (uint256 outputTokenAmount)
{
// Iterate through `tokens` perform a swap against the Uniswap
// pair contract for each `(tokens[i], tokens[i+1])`.
for (uint256 i = 0; i < tokens.length - 1; i++) {
(address inputToken, address outputToken) = (tokens[i], tokens[i + 1]);
// Compute the output token amount
outputTokenAmount = _computeUniswapOutputAmount(
pairAddress,
inputToken,
outputToken,
sellAmount
);
(uint256 amount0Out, uint256 amount1Out) = inputToken < outputToken
? (uint256(0), outputTokenAmount)
: (outputTokenAmount, uint256(0));
// The Uniswap pair contract will transfer the output tokens to
// the next pair contract if there is one, otherwise transfer to
// `recipient`.
address to = i < tokens.length - 2
? _computeUniswapPairAddress(outputToken, tokens[i + 2], isSushi)
: recipient;
// Execute the swap.
IUniswapV2Pair(pairAddress).swap(
amount0Out,
amount1Out,
to,
new bytes(0)
);
// To avoid recomputing the pair address of the next pair, store
// `to` in `pairAddress`.
pairAddress = to;
// The outputTokenAmount
sellAmount = outputTokenAmount;
}
}
// Computes the Uniswap/Sushiswap pair contract address for the
// given tokens.
function _computeUniswapPairAddress(
address tokenA,
address tokenB,
bool isSushi
)
internal
view
returns (address pairAddress)
{
// Tokens are lexicographically sorted in the Uniswap contract.
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
if (isSushi) {
// Use the Sushiswap factory address and codehash
return address(uint256(keccak256(abi.encodePacked(
hex'ff',
SUSHISWAP_FACTORY,
keccak256(abi.encodePacked(token0, token1)),
SUSHISWAP_PAIR_INIT_CODE_HASH
))));
} else {
// Use the Uniswap factory address and codehash
return address(uint256(keccak256(abi.encodePacked(
hex'ff',
UNISWAP_FACTORY,
keccak256(abi.encodePacked(token0, token1)),
UNISWAP_PAIR_INIT_CODE_HASH
))));
}
}
// 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)
{
// Input amount should be non-zero.
require(
inputAmount > 0,
"MultiplexUniswapV2::_computeUniswapOutputAmount/INSUFFICIENT_INPUT_AMOUNT"
);
// Query the reserves of the pair contract.
(uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairAddress).getReserves();
// Reserves must be non-zero.
require(
reserve0 > 0 && reserve1 > 0,
'MultiplexUniswapV2::_computeUniswapOutputAmount/INSUFFICIENT_LIQUIDITY'
);
// Tokens are lexicographically sorted in the Uniswap contract.
(uint256 inputReserve, uint256 outputReserve) = inputToken < outputToken
? (reserve0, reserve1)
: (reserve1, reserve0);
// Compute the output amount.
uint256 inputAmountWithFee = inputAmount.safeMul(997);
uint256 numerator = inputAmountWithFee.safeMul(outputReserve);
uint256 denominator = inputReserve.safeMul(1000).safeAdd(inputAmountWithFee);
return numerator / denominator;
}
}

View File

@ -0,0 +1,123 @@
// 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 "../../fixins/FixinTokenSpender.sol";
import "../interfaces/IMultiplexFeature.sol";
import "../interfaces/IUniswapV3Feature.sol";
abstract contract MultiplexUniswapV3 is
FixinTokenSpender
{
using LibSafeMathV06 for uint256;
function _batchSellUniswapV3(
IMultiplexFeature.BatchSellState memory state,
IMultiplexFeature.BatchSellParams memory params,
bytes memory wrappedCallData,
uint256 sellAmount
)
internal
{
bool success;
bytes memory resultData;
if (params.useSelfBalance) {
// If the tokens are held by `address(this)`, we call
// the `onlySelf` variant `_sellHeldTokenForTokenToUniswapV3`,
// which uses the Exchange Proxy's balance of input token.
(success, resultData) = address(this).call(
abi.encodeWithSelector(
IUniswapV3Feature._sellHeldTokenForTokenToUniswapV3.selector,
wrappedCallData,
sellAmount,
0,
params.recipient
)
);
} else {
// Otherwise, we self-delegatecall the normal variant
// `sellTokenForTokenToUniswapV3`, which pulls the input token
// from `msg.sender`.
(success, resultData) = address(this).delegatecall(
abi.encodeWithSelector(
IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector,
wrappedCallData,
sellAmount,
0,
params.recipient
)
);
}
if (success) {
// Decode the output token amount on success.
uint256 outputTokenAmount = abi.decode(resultData, (uint256));
// Increment the sold and bought amounts.
state.soldAmount = state.soldAmount.safeAdd(sellAmount);
state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount);
}
}
function _multiHopSellUniswapV3(
IMultiplexFeature.MultiHopSellState memory state,
bytes memory wrappedCallData
)
internal
{
bool success;
bytes memory resultData;
if (state.from == address(this)) {
// If the tokens are held by `address(this)`, we call
// the `onlySelf` variant `_sellHeldTokenForTokenToUniswapV3`,
// which uses the Exchange Proxy's balance of input token.
(success, resultData) = address(this).call(
abi.encodeWithSelector(
IUniswapV3Feature._sellHeldTokenForTokenToUniswapV3.selector,
wrappedCallData,
state.outputTokenAmount,
0,
state.to
)
);
} else {
// Otherwise, we self-delegatecall the normal variant
// `sellTokenForTokenToUniswapV3`, which pulls the input token
// from `msg.sender`.
(success, resultData) = address(this).delegatecall(
abi.encodeWithSelector(
IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector,
wrappedCallData,
state.outputTokenAmount,
0,
state.to
)
);
}
if (success) {
// Decode the output token amount on success.
state.outputTokenAmount = abi.decode(resultData, (uint256));
} else {
revert("MultiplexUniswapV3::_multiHopSellUniswapV3/SWAP_FAILED");
}
}
}

View File

@ -52,8 +52,10 @@ abstract contract NativeOrdersSettlement is
bytes32 orderHash;
// Maker of the order.
address maker;
// Taker of the order.
address taker;
// The address holding the taker tokens.
address payer;
// Recipient of the maker tokens.
address recipient;
// Maker token.
IERC20TokenV06 makerToken;
// Taker token.
@ -82,6 +84,22 @@ abstract contract NativeOrdersSettlement is
address sender;
}
/// @dev Params for `_fillRfqOrderPrivate()`
struct FillRfqOrderPrivateParams {
LibNativeOrder.RfqOrder order;
// The order signature.
LibSignature.Signature signature;
// Maximum taker token to fill this order with.
uint128 takerTokenFillAmount;
// The order taker.
address taker;
// Whether to use the Exchange Proxy's balance
// of taker tokens.
bool useSelfBalance;
// The recipient of the maker tokens.
address recipient;
}
// @dev Fill results returned by `_fillLimitOrderPrivate()` and
/// `_fillRfqOrderPrivate()`.
struct FillNativeOrderResults {
@ -154,12 +172,14 @@ abstract contract NativeOrdersSettlement is
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
order,
signature,
takerTokenFillAmount,
msg.sender
);
_fillRfqOrderPrivate(FillRfqOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: msg.sender,
useSelfBalance: false,
recipient: msg.sender
}));
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
@ -220,12 +240,14 @@ abstract contract NativeOrdersSettlement is
returns (uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
order,
signature,
takerTokenFillAmount,
msg.sender
);
_fillRfqOrderPrivate(FillRfqOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: msg.sender,
useSelfBalance: false,
recipient: msg.sender
}));
// Must have filled exactly the amount requested.
if (results.takerTokenFilledAmount < takerTokenFillAmount) {
LibNativeOrdersRichErrors.FillOrKillFailedError(
@ -260,33 +282,36 @@ abstract contract NativeOrdersSettlement is
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillLimitOrderPrivate(FillLimitOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: taker,
sender: sender
}));
_fillLimitOrderPrivate(FillLimitOrderPrivateParams(
order,
signature,
takerTokenFillAmount,
taker,
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`).
/// @dev Fill an RFQ order. Internal variant.
/// @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.
/// @param useSelfBalance Whether to use the ExchangeProxy's transient
/// balance of taker tokens to fill the order.
/// @param recipient The recipient of the maker tokens.
/// @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
address taker,
bool useSelfBalance,
address recipient
)
public
virtual
@ -294,12 +319,14 @@ abstract contract NativeOrdersSettlement is
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
FillNativeOrderResults memory results =
_fillRfqOrderPrivate(
_fillRfqOrderPrivate(FillRfqOrderPrivateParams(
order,
signature,
takerTokenFillAmount,
taker
);
taker,
useSelfBalance,
recipient
));
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
@ -387,7 +414,8 @@ abstract contract NativeOrdersSettlement is
SettleOrderInfo({
orderHash: orderInfo.orderHash,
maker: params.order.maker,
taker: params.taker,
payer: params.taker,
recipient: params.taker,
makerToken: IERC20TokenV06(params.order.makerToken),
takerToken: IERC20TokenV06(params.order.takerToken),
makerAmount: params.order.makerAmount,
@ -427,22 +455,14 @@ abstract contract NativeOrdersSettlement is
);
}
/// @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.
/// @dev Fill an RFQ order. Private variant.
/// @param params Function params.
/// @return results Results of the fill.
function _fillRfqOrderPrivate(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount,
address taker
)
function _fillRfqOrderPrivate(FillRfqOrderPrivateParams memory params)
private
returns (FillNativeOrderResults memory results)
{
LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(order);
LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(params.order);
// Must be fillable.
if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) {
@ -457,32 +477,41 @@ abstract contract NativeOrdersSettlement is
LibNativeOrdersStorage.getStorage();
// Must be fillable by the tx.origin.
if (order.txOrigin != tx.origin && !stor.originRegistry[order.txOrigin][tx.origin]) {
if (
params.order.txOrigin != tx.origin &&
!stor.originRegistry[params.order.txOrigin][tx.origin]
) {
LibNativeOrdersRichErrors.OrderNotFillableByOriginError(
orderInfo.orderHash,
tx.origin,
order.txOrigin
params.order.txOrigin
).rrevert();
}
}
// Must be fillable by the taker.
if (order.taker != address(0) && order.taker != taker) {
if (params.order.taker != address(0) && params.order.taker != params.taker) {
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderInfo.orderHash,
taker,
order.taker
params.taker,
params.order.taker
).rrevert();
}
// Signature must be valid for the order.
{
address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature);
if (signer != order.maker && !isValidOrderSigner(order.maker, signer)) {
address signer = LibSignature.getSignerOfHash(
orderInfo.orderHash,
params.signature
);
if (
signer != params.order.maker &&
!isValidOrderSigner(params.order.maker, signer)
) {
LibNativeOrdersRichErrors.OrderNotSignedByMakerError(
orderInfo.orderHash,
signer,
order.maker
params.order.maker
).rrevert();
}
}
@ -491,26 +520,27 @@ abstract contract NativeOrdersSettlement is
(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,
maker: params.order.maker,
payer: params.useSelfBalance ? address(this) : params.taker,
recipient: params.recipient,
makerToken: IERC20TokenV06(params.order.makerToken),
takerToken: IERC20TokenV06(params.order.takerToken),
makerAmount: params.order.makerAmount,
takerAmount: params.order.takerAmount,
takerTokenFillAmount: params.takerTokenFillAmount,
takerTokenFilledAmount: orderInfo.takerTokenFilledAmount
})
);
emit RfqOrderFilled(
orderInfo.orderHash,
order.maker,
taker,
address(order.makerToken),
address(order.takerToken),
params.order.maker,
params.taker,
address(params.order.makerToken),
address(params.order.takerToken),
results.takerTokenFilledAmount,
results.makerTokenFilledAmount,
order.pool
params.order.pool
);
}
@ -549,19 +579,28 @@ abstract contract NativeOrdersSettlement is
// function if the order is cancelled.
settleInfo.takerTokenFilledAmount.safeAdd128(takerTokenFilledAmount);
// Transfer taker -> maker.
_transferERC20TokensFrom(
settleInfo.takerToken,
settleInfo.taker,
settleInfo.maker,
takerTokenFilledAmount
);
if (settleInfo.payer == address(this)) {
// Transfer this -> maker.
_transferERC20Tokens(
settleInfo.takerToken,
settleInfo.maker,
takerTokenFilledAmount
);
} else {
// Transfer taker -> maker.
_transferERC20TokensFrom(
settleInfo.takerToken,
settleInfo.payer,
settleInfo.maker,
takerTokenFilledAmount
);
}
// Transfer maker -> taker.
// Transfer maker -> recipient.
_transferERC20TokensFrom(
settleInfo.makerToken,
settleInfo.maker,
settleInfo.taker,
settleInfo.recipient,
makerTokenFilledAmount
);
}

View File

@ -137,8 +137,8 @@ contract FillQuoteTransformer is
/// @dev Mask of the lower 255 bits of a uint256 value.
uint256 private constant LOWER_255_BITS = HIGH_BIT - 1;
/// @dev If `refundReceiver` is set to this address, unpsent
/// protocol fees will be sent to the taker.
address private constant REFUND_RECEIVER_TAKER = address(1);
/// protocol fees will be sent to the transform recipient.
address private constant REFUND_RECEIVER_RECIPIENT = address(1);
/// @dev If `refundReceiver` is set to this address, unpsent
/// protocol fees will be sent to the sender.
address private constant REFUND_RECEIVER_SENDER = address(2);
@ -272,8 +272,8 @@ contract FillQuoteTransformer is
// Refund unspent protocol fees.
if (state.ethRemaining > 0 && data.refundReceiver != address(0)) {
bool transferSuccess;
if (data.refundReceiver == REFUND_RECEIVER_TAKER) {
(transferSuccess,) = context.taker.call{value: state.ethRemaining}("");
if (data.refundReceiver == REFUND_RECEIVER_RECIPIENT) {
(transferSuccess,) = context.recipient.call{value: state.ethRemaining}("");
} else if (data.refundReceiver == REFUND_RECEIVER_SENDER) {
(transferSuccess,) = context.sender.call{value: state.ethRemaining}("");
} else {

View File

@ -30,9 +30,9 @@ interface IERC20Transformer {
struct TransformContext {
// The caller of `TransformERC20.transformERC20()`.
address payable sender;
// taker The taker address, which may be distinct from `sender` in the case
// The recipient address, which may be distinct from `sender` e.g. in
// meta-transactions.
address payable taker;
address payable recipient;
// Arbitrary data to pass to the transformer.
bytes data;
}

View File

@ -41,7 +41,7 @@ contract LogMetadataTransformer is
override
returns (bytes4 success)
{
emit TransformerMetadata(context.sender, context.taker, context.data);
emit TransformerMetadata(context.sender, context.recipient, context.data);
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@ -75,7 +75,7 @@ contract PayTakerTransformer is
amount = data.tokens[i].getTokenBalanceOf(address(this));
}
if (amount != 0) {
data.tokens[i].transformerTransfer(context.taker, amount);
data.tokens[i].transformerTransfer(context.recipient, amount);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;

View File

@ -33,7 +33,7 @@ contract TestFillQuoteTransformerHost is
TestMintableERC20Token inputToken,
uint256 inputTokenAmount,
address payable sender,
address payable taker,
address payable recipient,
bytes calldata data
)
external
@ -47,7 +47,7 @@ contract TestFillQuoteTransformerHost is
transformer,
IERC20Transformer.TransformContext({
sender: sender,
taker: taker,
recipient: recipient,
data: data
})
);

View File

@ -46,16 +46,6 @@ contract TestLiquidityProvider {
uint256 inputTokenBalance
);
IERC20TokenV06 public immutable xAsset;
IERC20TokenV06 public immutable yAsset;
constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_)
public
{
xAsset = xAsset_;
yAsset = yAsset_;
}
receive() external payable {}
/// @dev Trades `inputToken` for `outputToken`. The amount of `inputToken`
@ -83,6 +73,8 @@ contract TestLiquidityProvider {
minBuyAmount,
IERC20TokenV06(inputToken).balanceOf(address(this))
);
uint256 outputTokenBalance = IERC20TokenV06(outputToken).balanceOf(address(this));
IERC20TokenV06(outputToken).transfer(recipient, outputTokenBalance);
}
/// @dev Trades ETH for token. ETH must be sent to the contract prior to
@ -106,6 +98,8 @@ contract TestLiquidityProvider {
minBuyAmount,
address(this).balance
);
uint256 outputTokenBalance = IERC20TokenV06(outputToken).balanceOf(address(this));
IERC20TokenV06(outputToken).transfer(recipient, outputTokenBalance);
}
/// @dev Trades token for ETH. The token must be sent to the contract prior
@ -129,5 +123,6 @@ contract TestLiquidityProvider {
minBuyAmount,
IERC20TokenV06(inputToken).balanceOf(address(this))
);
recipient.transfer(address(this).balance);
}
}

View File

@ -89,7 +89,9 @@ contract TestMetaTransactionsNativeOrdersFeature is
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount,
address taker
address taker,
bool /* useSelfBalance */,
address /* recipient */
)
public
override

View File

@ -57,7 +57,7 @@ contract TestMintTokenERC20Transformer is
address(this),
msg.sender,
context.sender,
context.taker,
context.recipient,
context.data,
LibERC20Transformer.isTokenETH(data.inputToken)
? address(this).balance
@ -71,15 +71,22 @@ contract TestMintTokenERC20Transformer is
data.inputToken.transfer(address(0), data.burnAmount);
}
// Mint output tokens.
if (LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) {
context.taker.transfer(data.mintAmount);
} else {
data.outputToken.mint(
context.taker,
data.mintAmount
);
// Burn fees from output.
data.outputToken.burn(context.taker, data.feeAmount);
if (!LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) {
if (data.feeAmount > data.mintAmount) {
data.outputToken.burn(
context.recipient,
data.feeAmount - data.mintAmount
);
} else {
data.outputToken.mint(
address(this),
data.mintAmount
);
data.outputToken.burn(
context.recipient,
data.feeAmount
);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}

View File

@ -22,6 +22,12 @@ pragma experimental ABIEncoderV2;
contract TestMintableERC20Token {
event Transfer(
address token,
address from,
address to,
uint256 value
);
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
@ -81,6 +87,7 @@ contract TestMintableERC20Token {
require(balanceOf[from] >= amount, "TestMintableERC20Token/INSUFFICIENT_FUNDS");
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(address(this), from, to, amount);
return true;
}

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.6;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./TestUniswapV2Pool.sol";
contract TestUniswapV2Factory {
struct CreationParameters {
IERC20TokenV06 token0;
IERC20TokenV06 token1;
}
event PoolCreated(TestUniswapV2Pool pool);
bytes32 public immutable POOL_INIT_CODE_HASH;
mapping (IERC20TokenV06 => mapping (IERC20TokenV06 => TestUniswapV2Pool)) public getPool;
CreationParameters public creationParameters;
constructor() public {
POOL_INIT_CODE_HASH = keccak256(type(TestUniswapV2Pool).creationCode);
}
function createPool(IERC20TokenV06 tokenA, IERC20TokenV06 tokenB)
external
returns (TestUniswapV2Pool pool)
{
(IERC20TokenV06 token0, IERC20TokenV06 token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
require(
getPool[token0][token1] == TestUniswapV2Pool(0),
"TestUniswapV2Factory/POOL_ALREADY_EXISTS"
);
creationParameters = CreationParameters({
token0: token0,
token1: token1
});
pool = new TestUniswapV2Pool
{ salt: keccak256(abi.encodePacked(token0, token1)) }();
getPool[token0][token1] = pool;
getPool[token1][token0] = pool;
emit PoolCreated(pool);
}
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.6;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../src/vendor/IUniswapV2Pair.sol";
interface IUniswapV2PoolDeployer {
struct CreationParameters {
IERC20TokenV06 token0;
IERC20TokenV06 token1;
}
function creationParameters() external view returns (CreationParameters memory);
}
contract TestUniswapV2Pool is IUniswapV2Pair {
IERC20TokenV06 public immutable token0;
IERC20TokenV06 public immutable token1;
uint112 reserve0;
uint112 reserve1;
uint32 blockTimestampLast;
constructor() public {
IUniswapV2PoolDeployer.CreationParameters memory params =
IUniswapV2PoolDeployer(msg.sender).creationParameters();
(token0, token1) = (params.token0, params.token1);
}
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata /* data */
)
external
override
{
if (amount0Out > 0) {
token0.transfer(to, amount0Out);
}
if (amount1Out > 0) {
token1.transfer(to, amount1Out);
}
}
function setReserves(
uint112 reserve0_,
uint112 reserve1_,
uint32 blockTimestampLast_
)
external
{
reserve0 = reserve0_;
reserve1 = reserve1_;
blockTimestampLast = blockTimestampLast_;
}
function getReserves()
external
override
view
returns (uint112, uint112, uint32)
{
return (reserve0, reserve1, blockTimestampLast);
}
}

View File

@ -26,11 +26,15 @@ import "./TestMintableERC20Token.sol";
contract TestWeth is
TestMintableERC20Token
{
event Deposit(address owner, uint256 value);
event Withdrawal(address owner, uint256 value);
function deposit()
external
payable
{
this.mint(msg.sender, msg.value);
emit Deposit(msg.sender, msg.value);
}
function depositTo(address owner)
@ -38,6 +42,7 @@ contract TestWeth is
payable
{
this.mint(owner, msg.value);
emit Deposit(owner, msg.value);
}
function withdraw(uint256 amount)
@ -46,5 +51,6 @@ contract TestWeth is
require(balanceOf[msg.sender] >= amount, "TestWeth/INSUFFICIENT_FUNDS");
balanceOf[msg.sender] -= amount;
msg.sender.transfer(amount);
emit Withdrawal(msg.sender, amount);
}
}

View File

@ -53,7 +53,7 @@ contract TestWethTransformerHost is
transformer,
IERC20Transformer.TransformContext({
sender: msg.sender,
taker: msg.sender,
recipient: msg.sender,
data: data
})
);

View File

@ -43,7 +43,7 @@
"config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|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|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|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|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json"
},
"repository": {
"type": "git",

View File

@ -104,6 +104,12 @@ import * as MixinUniswapV3 from '../test/generated-artifacts/MixinUniswapV3.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 MultiplexLiquidityProvider from '../test/generated-artifacts/MultiplexLiquidityProvider.json';
import * as MultiplexOtc from '../test/generated-artifacts/MultiplexOtc.json';
import * as MultiplexRfq from '../test/generated-artifacts/MultiplexRfq.json';
import * as MultiplexTransformERC20 from '../test/generated-artifacts/MultiplexTransformERC20.json';
import * as MultiplexUniswapV2 from '../test/generated-artifacts/MultiplexUniswapV2.json';
import * as MultiplexUniswapV3 from '../test/generated-artifacts/MultiplexUniswapV3.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';
@ -151,6 +157,8 @@ import * as TestTransformerBase from '../test/generated-artifacts/TestTransforme
import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json';
import * as TestTransformerDeployerTransformer from '../test/generated-artifacts/TestTransformerDeployerTransformer.json';
import * as TestTransformerHost from '../test/generated-artifacts/TestTransformerHost.json';
import * as TestUniswapV2Factory from '../test/generated-artifacts/TestUniswapV2Factory.json';
import * as TestUniswapV2Pool from '../test/generated-artifacts/TestUniswapV2Pool.json';
import * as TestUniswapV3Factory from '../test/generated-artifacts/TestUniswapV3Factory.json';
import * as TestUniswapV3Feature from '../test/generated-artifacts/TestUniswapV3Feature.json';
import * as TestUniswapV3Pool from '../test/generated-artifacts/TestUniswapV3Pool.json';
@ -192,7 +200,6 @@ export const artifacts = {
BootstrapFeature: BootstrapFeature as ContractArtifact,
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
MultiplexFeature: MultiplexFeature as ContractArtifact,
NativeOrdersFeature: NativeOrdersFeature as ContractArtifact,
OtcOrdersFeature: OtcOrdersFeature as ContractArtifact,
OwnableFeature: OwnableFeature as ContractArtifact,
@ -219,6 +226,13 @@ export const artifacts = {
IUniswapV3Feature: IUniswapV3Feature as ContractArtifact,
LibNativeOrder: LibNativeOrder as ContractArtifact,
LibSignature: LibSignature as ContractArtifact,
MultiplexFeature: MultiplexFeature as ContractArtifact,
MultiplexLiquidityProvider: MultiplexLiquidityProvider as ContractArtifact,
MultiplexOtc: MultiplexOtc as ContractArtifact,
MultiplexRfq: MultiplexRfq as ContractArtifact,
MultiplexTransformERC20: MultiplexTransformERC20 as ContractArtifact,
MultiplexUniswapV2: MultiplexUniswapV2 as ContractArtifact,
MultiplexUniswapV3: MultiplexUniswapV3 as ContractArtifact,
NativeOrdersCancellation: NativeOrdersCancellation as ContractArtifact,
NativeOrdersInfo: NativeOrdersInfo as ContractArtifact,
NativeOrdersProtocolFees: NativeOrdersProtocolFees as ContractArtifact,
@ -320,6 +334,8 @@ export const artifacts = {
TestTransformerBase: TestTransformerBase as ContractArtifact,
TestTransformerDeployerTransformer: TestTransformerDeployerTransformer as ContractArtifact,
TestTransformerHost: TestTransformerHost as ContractArtifact,
TestUniswapV2Factory: TestUniswapV2Factory as ContractArtifact,
TestUniswapV2Pool: TestUniswapV2Pool as ContractArtifact,
TestUniswapV3Factory: TestUniswapV3Factory as ContractArtifact,
TestUniswapV3Feature: TestUniswapV3Feature as ContractArtifact,
TestUniswapV3Pool: TestUniswapV3Pool as ContractArtifact,

View File

@ -72,8 +72,6 @@ blockchainTests('LiquidityProvider feature', env => {
env.provider,
env.txDefaults,
artifacts,
token.address,
weth.address,
);
});
blockchainTests.resets('Sandbox', () => {

View File

@ -354,6 +354,8 @@ blockchainTests.resets('MetaTransactions feature', env => {
inputTokenAmount: args.inputTokenAmount,
minOutputTokenAmount: args.minOutputTokenAmount,
transformations: args.transformations,
useSelfBalance: false,
recipient: mtx.signer,
})
.getABIEncodedTransactionData();
return expect(tx).to.revertWith(

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ import {
blockchainTests.resets('OtcOrdersFeature', env => {
const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT: ZERO } = constants;
const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
let maker: string;
let taker: string;
let notMaker: string;
@ -309,7 +310,7 @@ blockchainTests.resets('OtcOrdersFeature', env => {
const order = getTestOtcOrder();
await testUtils.prepareBalancesForOrdersAsync([order], taker);
const tx = zeroEx
.fillOtcOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount, false)
.fillOtcOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
// This will revert at the language level because the fill function is not payable.
return expect(tx).to.be.rejectedWith('revert');
@ -395,7 +396,7 @@ blockchainTests.resets('OtcOrdersFeature', env => {
it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => {
const order = getTestOtcOrder();
const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true);
return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_UNWRAP_WETH');
return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH');
});
it('allows for fills on orders signed by a approved signer', async () => {
@ -415,7 +416,7 @@ blockchainTests.resets('OtcOrdersFeature', env => {
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
// fill should succeed
const receipt = await zeroEx
.fillOtcOrder(order, sig, order.takerAmount, false)
.fillOtcOrder(order, sig, order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(
receipt.logs,
@ -445,9 +446,7 @@ blockchainTests.resets('OtcOrdersFeature', env => {
.registerAllowedOrderSigner(contractWalletSigner, false)
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
// fill should revert
const tx = zeroEx
.fillOtcOrder(order, sig, order.takerAmount, false)
.awaitTransactionSuccessAsync({ from: taker });
const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(
order.getHash(),
@ -465,16 +464,14 @@ blockchainTests.resets('OtcOrdersFeature', env => {
// need to provide contract wallet with a balance
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
// fill should revert
const tx = zeroEx
.fillOtcOrder(order, sig, order.takerAmount, false)
.awaitTransactionSuccessAsync({ from: taker });
const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker),
);
});
});
describe('fillOtcOrderWithEth()', () => {
it('Can fill an order with ETH', async () => {
it('Can fill an order with ETH (takerToken=WETH)', async () => {
const order = getTestOtcOrder({ takerToken: wethToken.address });
const receipt = await testUtils.fillOtcOrderWithEthAsync(order);
verifyEventsFromLogs(
@ -484,7 +481,25 @@ blockchainTests.resets('OtcOrdersFeature', env => {
);
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
});
it('Can partially fill an order with ETH', async () => {
it('Can fill an order with ETH (takerToken=ETH)', async () => {
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const receipt = await testUtils.fillOtcOrderWithEthAsync(order);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createOtcOrderFilledEventArgs(order)],
IZeroExEvents.OtcOrderFilled,
);
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
.balanceOf(taker)
.callAsync();
expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount);
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
order.takerAmount,
);
});
it('Can partially fill an order with ETH (takerToken=WETH)', async () => {
const order = getTestOtcOrder({ takerToken: wethToken.address });
const fillAmount = order.takerAmount.minus(1);
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
@ -495,7 +510,27 @@ blockchainTests.resets('OtcOrdersFeature', env => {
);
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount);
});
it('Can refund excess ETH is msg.value > order.takerAmount', async () => {
it('Can partially fill an order with ETH (takerToken=ETH)', async () => {
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
const fillAmount = order.takerAmount.minus(1);
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createOtcOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.OtcOrderFilled,
);
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(order, fillAmount);
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
.balanceOf(taker)
.callAsync();
expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount);
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
takerTokenFilledAmount,
);
});
it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=WETH)', async () => {
const order = getTestOtcOrder({ takerToken: wethToken.address });
const fillAmount = order.takerAmount.plus(420);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
@ -509,10 +544,34 @@ blockchainTests.resets('OtcOrdersFeature', env => {
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.takerAmount);
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
});
it('Cannot fill an order if taker token is not WETH', async () => {
it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=ETH)', async () => {
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
const fillAmount = order.takerAmount.plus(420);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createOtcOrderFilledEventArgs(order)],
IZeroExEvents.OtcOrderFilled,
);
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter), 'taker eth balance').to.bignumber.equal(
order.takerAmount,
);
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
.balanceOf(taker)
.callAsync();
expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount);
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
order.takerAmount,
);
});
it('Cannot fill an order if taker token is not ETH or WETH', async () => {
const order = getTestOtcOrder();
const tx = testUtils.fillOtcOrderWithEthAsync(order);
return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_WRAP_ETH');
return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN');
});
});
@ -578,15 +637,22 @@ blockchainTests.resets('OtcOrdersFeature', env => {
const order = getTestOtcOrder({ taker, txOrigin });
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByTakerError(order.getHash(), notTaker, taker),
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, taker),
);
});
it('cannot fill order with bad maker signature', async () => {
const order = getTestOtcOrder({ taker, txOrigin });
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = testUtils.fillTakerSignedOtcOrderAsync(order.clone({ chainId: 1234 }));
const anotherOrder = getTestOtcOrder({ taker, txOrigin });
await testUtils.prepareBalancesForOrdersAsync([order], taker);
const tx = zeroEx
.fillTakerSignedOtcOrder(
order,
await anotherOrder.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
)
.awaitTransactionSuccessAsync({ from: txOrigin });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
);
@ -600,7 +666,6 @@ blockchainTests.resets('OtcOrdersFeature', env => {
order,
await order.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
false,
)
.awaitTransactionSuccessAsync({ from: txOrigin, value: 1 });
// This will revert at the language level because the fill function is not payable.
@ -702,51 +767,102 @@ blockchainTests.resets('OtcOrdersFeature', env => {
it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => {
const order = getTestOtcOrder({ taker, txOrigin });
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true);
return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_UNWRAP_WETH');
return expect(tx).to.revertWith('OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH');
});
});
it('allows for fills on orders signed by a approved signer (taker)', async () => {
const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address });
await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address);
// allow signer
await contractWallet
.registerAllowedOrderSigner(contractWalletSigner, true)
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
// fill should succeed
const receipt = await zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(
env.provider,
SignatureType.EthSign,
contractWalletSigner,
),
false,
describe('batchFillTakerSignedOtcOrders()', () => {
it('Fills multiple orders', async () => {
const order1 = getTestOtcOrder({ taker, txOrigin });
const order2 = getTestOtcOrder({
taker: notTaker,
txOrigin,
nonceBucket: order1.nonceBucket,
nonce: order1.nonce.plus(1),
});
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
const tx = await zeroEx
.batchFillTakerSignedOtcOrders(
[order1, order2],
[
await order1.getSignatureWithProviderAsync(env.provider),
await order2.getSignatureWithProviderAsync(env.provider),
],
[
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
],
[false, false],
)
.awaitTransactionSuccessAsync({ from: txOrigin });
verifyEventsFromLogs(
receipt.logs,
[testUtils.createOtcOrderFilledEventArgs(order)],
tx.logs,
[testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)],
IZeroExEvents.OtcOrderFilled,
);
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
});
it(`doesn't allow fills with an unapproved signer (taker)`, async () => {
const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address });
await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address);
// fill should succeed
const tx = zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
false,
it('Fills multiple orders and unwraps WETH', async () => {
const order1 = getTestOtcOrder({ taker, txOrigin });
const order2 = getTestOtcOrder({
taker: notTaker,
txOrigin,
nonceBucket: order1.nonceBucket,
nonce: order1.nonce.plus(1),
makerToken: wethToken.address,
makerAmount: new BigNumber('1e18'),
});
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order2.makerAmount });
const tx = await zeroEx
.batchFillTakerSignedOtcOrders(
[order1, order2],
[
await order1.getSignatureWithProviderAsync(env.provider),
await order2.getSignatureWithProviderAsync(env.provider),
],
[
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
],
[false, true],
)
.awaitTransactionSuccessAsync({ from: txOrigin });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByTakerError(order.getHash(), notTaker, order.taker),
verifyEventsFromLogs(
tx.logs,
[testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)],
IZeroExEvents.OtcOrderFilled,
);
});
it('Skips over unfillable orders', async () => {
const order1 = getTestOtcOrder({ taker, txOrigin });
const order2 = getTestOtcOrder({
taker: notTaker,
txOrigin,
nonceBucket: order1.nonceBucket,
nonce: order1.nonce.plus(1),
});
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
const tx = await zeroEx
.batchFillTakerSignedOtcOrders(
[order1, order2],
[
await order1.getSignatureWithProviderAsync(env.provider),
await order2.getSignatureWithProviderAsync(env.provider),
],
[
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), // Invalid signature for order2
],
[false, false],
)
.awaitTransactionSuccessAsync({ from: txOrigin });
verifyEventsFromLogs(
tx.logs,
[testUtils.createOtcOrderFilledEventArgs(order1)],
IZeroExEvents.OtcOrderFilled,
);
});
});

View File

@ -243,6 +243,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount,
minOutputTokenAmount,
transformations: [transformation],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
verifyEventsFromLogs(
@ -281,7 +283,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
const minOutputTokenAmount = getRandomInteger(1, '1e18');
const outputTokenMintAmount = minOutputTokenAmount;
const callValue = outputTokenMintAmount.times(2);
const callValue = outputTokenMintAmount;
const transformation = createMintTokenTransformation({
outputTokenMintAmount,
inputTokenBurnAmunt: inputTokenAmount,
@ -296,6 +298,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount,
minOutputTokenAmount,
transformations: [transformation],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
verifyEventsFromLogs(
@ -352,6 +356,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount,
minOutputTokenAmount,
transformations: [transformation],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
verifyEventsFromLogs(
@ -406,6 +412,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenBurnAmunt: inputTokenAmount,
}),
],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error(
@ -438,6 +446,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenBurnAmunt: inputTokenAmount,
}),
],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError(
@ -475,6 +485,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount,
minOutputTokenAmount,
transformations,
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
verifyEventsFromLogs(
@ -520,6 +532,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount,
minOutputTokenAmount,
transformations,
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
return expect(tx).to.revertWith(
@ -549,6 +563,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount: MAX_UINT256,
minOutputTokenAmount,
transformations: [transformation],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: callValue });
verifyEventsFromLogs(
@ -584,6 +600,8 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenAmount: MAX_UINT256,
minOutputTokenAmount,
transformations: [transformation],
useSelfBalance: false,
recipient: taker,
})
.awaitTransactionSuccessAsync({ value: ethAttchedAmount });
verifyEventsFromLogs(

View File

@ -70,7 +70,7 @@ blockchainTests.resets('Full migration', env => {
TransformERC20: {
contractType: ITransformERC20FeatureContract,
fns: [
'transformERC20',
// 'transformERC20', TODO
'_transformERC20',
'createTransformWallet',
'getTransformWallet',
@ -148,6 +148,9 @@ blockchainTests.resets('Full migration', env => {
if (item.type === 'byte') {
return hexUtils.random(1);
}
if (item.type === 'bool') {
return Math.random() > 0.5;
}
if (/^bytes$/.test(item.type)) {
return hexUtils.random(_.random(0, 128));
}

View File

@ -89,7 +89,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
@ -119,7 +119,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
@ -149,7 +149,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq({

View File

@ -80,7 +80,7 @@ blockchainTests.resets('PayTakerTransformer', env => {
await host
.rawExecuteTransform(transformer.address, {
data,
taker,
recipient: taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync();
@ -102,7 +102,7 @@ blockchainTests.resets('PayTakerTransformer', env => {
await host
.rawExecuteTransform(transformer.address, {
data,
taker,
recipient: taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync();
@ -124,7 +124,7 @@ blockchainTests.resets('PayTakerTransformer', env => {
await host
.rawExecuteTransform(transformer.address, {
data,
taker,
recipient: taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync();
@ -146,7 +146,7 @@ blockchainTests.resets('PayTakerTransformer', env => {
await host
.rawExecuteTransform(transformer.address, {
data,
taker,
recipient: taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync();

View File

@ -70,7 +70,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost);
@ -92,7 +92,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost);
@ -112,7 +112,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => {
.rawExecuteTransform(transformer.address, {
data,
sender: randomAddress(),
taker: randomAddress(),
recipient: randomAddress(),
})
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq({

View File

@ -156,14 +156,23 @@ export class NativeOrdersTestEnvironment {
unwrapWeth: boolean = false,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
unwrapWeth,
)
.awaitTransactionSuccessAsync({ from: taker });
if (unwrapWeth) {
return this.zeroEx
.fillOtcOrderForEth(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker });
} else {
return this.zeroEx
.fillOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker });
}
}
public async fillTakerSignedOtcOrderAsync(
@ -173,14 +182,23 @@ export class NativeOrdersTestEnvironment {
unwrapWeth: boolean = false,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker),
unwrapWeth,
)
.awaitTransactionSuccessAsync({ from: origin });
if (unwrapWeth) {
return this.zeroEx
.fillTakerSignedOtcOrderForEth(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker),
)
.awaitTransactionSuccessAsync({ from: origin });
} else {
return this.zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker),
)
.awaitTransactionSuccessAsync({ from: origin });
}
}
public async fillOtcOrderWithEthAsync(

View File

@ -102,6 +102,12 @@ export * from '../test/generated-wrappers/mixin_uniswap_v3';
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/multiplex_liquidity_provider';
export * from '../test/generated-wrappers/multiplex_otc';
export * from '../test/generated-wrappers/multiplex_rfq';
export * from '../test/generated-wrappers/multiplex_transform_erc20';
export * from '../test/generated-wrappers/multiplex_uniswap_v2';
export * from '../test/generated-wrappers/multiplex_uniswap_v3';
export * from '../test/generated-wrappers/native_orders_cancellation';
export * from '../test/generated-wrappers/native_orders_feature';
export * from '../test/generated-wrappers/native_orders_info';
@ -149,6 +155,8 @@ export * from '../test/generated-wrappers/test_transform_erc20';
export * from '../test/generated-wrappers/test_transformer_base';
export * from '../test/generated-wrappers/test_transformer_deployer_transformer';
export * from '../test/generated-wrappers/test_transformer_host';
export * from '../test/generated-wrappers/test_uniswap_v2_factory';
export * from '../test/generated-wrappers/test_uniswap_v2_pool';
export * from '../test/generated-wrappers/test_uniswap_v3_factory';
export * from '../test/generated-wrappers/test_uniswap_v3_feature';
export * from '../test/generated-wrappers/test_uniswap_v3_pool';

View File

@ -135,6 +135,12 @@
"test/generated-artifacts/MixinZeroExBridge.json",
"test/generated-artifacts/MooniswapLiquidityProvider.json",
"test/generated-artifacts/MultiplexFeature.json",
"test/generated-artifacts/MultiplexLiquidityProvider.json",
"test/generated-artifacts/MultiplexOtc.json",
"test/generated-artifacts/MultiplexRfq.json",
"test/generated-artifacts/MultiplexTransformERC20.json",
"test/generated-artifacts/MultiplexUniswapV2.json",
"test/generated-artifacts/MultiplexUniswapV3.json",
"test/generated-artifacts/NativeOrdersCancellation.json",
"test/generated-artifacts/NativeOrdersFeature.json",
"test/generated-artifacts/NativeOrdersInfo.json",
@ -182,6 +188,8 @@
"test/generated-artifacts/TestTransformerBase.json",
"test/generated-artifacts/TestTransformerDeployerTransformer.json",
"test/generated-artifacts/TestTransformerHost.json",
"test/generated-artifacts/TestUniswapV2Factory.json",
"test/generated-artifacts/TestUniswapV2Pool.json",
"test/generated-artifacts/TestUniswapV3Factory.json",
"test/generated-artifacts/TestUniswapV3Feature.json",
"test/generated-artifacts/TestUniswapV3Pool.json",

View File

@ -49,20 +49,6 @@ export class OrderNotSignedByMakerError extends RevertError {
}
}
export class OrderNotSignedByTakerError extends RevertError {
constructor(orderHash?: string, signer?: string, taker?: string) {
super(
'OrderNotSignedByTakerError',
'OrderNotSignedByTakerError(bytes32 orderHash, address signer, address taker)',
{
orderHash,
signer,
taker,
},
);
}
}
export class InvalidSignerError extends RevertError {
constructor(maker?: string, signer?: string) {
super('InvalidSignerError', 'InvalidSignerError(address maker, address signer)', {
@ -152,7 +138,6 @@ const types = [
OrderNotFillableByOriginError,
OrderNotFillableError,
OrderNotSignedByMakerError,
OrderNotSignedByTakerError,
OrderNotFillableBySenderError,
OrderNotFillableByTakerError,
CancelSaltTooLowError,