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:
parent
692231c2ea
commit
b46eeadc64
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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] });
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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`.
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
})
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
44
contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol
Normal file
44
contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol
Normal 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);
|
||||
}
|
||||
}
|
67
contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol
Normal file
67
contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ contract TestWethTransformerHost is
|
||||
transformer,
|
||||
IERC20Transformer.TransformContext({
|
||||
sender: msg.sender,
|
||||
taker: msg.sender,
|
||||
recipient: msg.sender,
|
||||
data: data
|
||||
})
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -72,8 +72,6 @@ blockchainTests('LiquidityProvider feature', env => {
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
token.address,
|
||||
weth.address,
|
||||
);
|
||||
});
|
||||
blockchainTests.resets('Sandbox', () => {
|
||||
|
@ -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
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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();
|
||||
|
@ -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({
|
||||
|
@ -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(
|
||||
|
@ -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';
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user