diff --git a/contracts/erc20/lib/forge-std b/contracts/erc20/lib/forge-std index a2edd39db9..fc560fa34f 160000 --- a/contracts/erc20/lib/forge-std +++ b/contracts/erc20/lib/forge-std @@ -1 +1 @@ -Subproject commit a2edd39db95df7e9dd3f9ef9edc8c55fefddb6df +Subproject commit fc560fa34fa12a335a50c35d92e55a6628ca467c diff --git a/contracts/zero-ex/compiler.json b/contracts/zero-ex/compiler.json index c5baa53b85..f475ea0e80 100644 --- a/contracts/zero-ex/compiler.json +++ b/contracts/zero-ex/compiler.json @@ -31,6 +31,7 @@ "./contracts/src/features/FundRecoveryFeature.sol", "./contracts/src/features/LiquidityProviderFeature.sol", "./contracts/src/features/MetaTransactionsFeature.sol", + "./contracts/src/features/MetaTransactionsFeatureV2.sol", "./contracts/src/features/NativeOrdersFeature.sol", "./contracts/src/features/OtcOrdersFeature.sol", "./contracts/src/features/OwnableFeature.sol", @@ -48,6 +49,7 @@ "./contracts/src/features/interfaces/IFundRecoveryFeature.sol", "./contracts/src/features/interfaces/ILiquidityProviderFeature.sol", "./contracts/src/features/interfaces/IMetaTransactionsFeature.sol", + "./contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol", "./contracts/src/features/interfaces/IMultiplexFeature.sol", "./contracts/src/features/interfaces/INativeOrdersEvents.sol", "./contracts/src/features/interfaces/INativeOrdersFeature.sol", diff --git a/contracts/zero-ex/contracts/deps/forge-std b/contracts/zero-ex/contracts/deps/forge-std index a2edd39db9..fc560fa34f 160000 --- a/contracts/zero-ex/contracts/deps/forge-std +++ b/contracts/zero-ex/contracts/deps/forge-std @@ -1 +1 @@ -Subproject commit a2edd39db95df7e9dd3f9ef9edc8c55fefddb6df +Subproject commit fc560fa34fa12a335a50c35d92e55a6628ca467c diff --git a/contracts/zero-ex/contracts/src/IZeroEx.sol b/contracts/zero-ex/contracts/src/IZeroEx.sol index 2d7953a729..19632126e9 100644 --- a/contracts/zero-ex/contracts/src/IZeroEx.sol +++ b/contracts/zero-ex/contracts/src/IZeroEx.sol @@ -20,6 +20,7 @@ import "./features/interfaces/ISimpleFunctionRegistryFeature.sol"; import "./features/interfaces/ITokenSpenderFeature.sol"; import "./features/interfaces/ITransformERC20Feature.sol"; import "./features/interfaces/IMetaTransactionsFeature.sol"; +import "./features/interfaces/IMetaTransactionsFeatureV2.sol"; import "./features/interfaces/IUniswapFeature.sol"; import "./features/interfaces/IUniswapV3Feature.sol"; import "./features/interfaces/IPancakeSwapFeature.sol"; @@ -39,6 +40,7 @@ interface IZeroEx is ISimpleFunctionRegistryFeature, ITransformERC20Feature, IMetaTransactionsFeature, + IMetaTransactionsFeatureV2, IUniswapFeature, IUniswapV3Feature, IPancakeSwapFeature, diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeatureV2.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeatureV2.sol new file mode 100644 index 0000000000..ff3e289932 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeatureV2.sol @@ -0,0 +1,616 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + Copyright 2023 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/src/IEtherToken.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../errors/LibMetaTransactionsRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../fixins/FixinReentrancyGuard.sol"; +import "../fixins/FixinTokenSpender.sol"; +import "../fixins/FixinEIP712.sol"; +import "../migrations/LibMigrate.sol"; +import "../storage/LibMetaTransactionsV2Storage.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IMetaTransactionsFeatureV2.sol"; +import "./interfaces/IMultiplexFeature.sol"; +import "./interfaces/INativeOrdersFeature.sol"; +import "./interfaces/ITransformERC20Feature.sol"; +import "./libs/LibSignature.sol"; + +/// @dev MetaTransactions feature. +contract MetaTransactionsFeatureV2 is + IFeature, + IMetaTransactionsFeatureV2, + FixinCommon, + FixinReentrancyGuard, + FixinEIP712, + FixinTokenSpender +{ + using LibBytesV06 for bytes; + using LibRichErrorsV06 for bytes; + + /// @dev Describes the state of a meta transaction. + struct ExecuteState { + // Sender of the meta-transaction. + address sender; + // Hash of the meta-transaction data. + bytes32 hash; + // The meta-transaction data. + MetaTransactionDataV2 mtx; + // The meta-transaction signature (by `mtx.signer`). + LibSignature.Signature signature; + // The selector of the function being called. + bytes4 selector; + // The ETH balance of this contract before performing the call. + uint256 selfBalance; + // The block number at which the meta-transaction was executed. + uint256 executedBlockNumber; + } + + /// @dev Arguments for a `TransformERC20.transformERC20()` call. + struct ExternalTransformERC20Args { + IERC20Token inputToken; + IERC20Token outputToken; + uint256 inputTokenAmount; + uint256 minOutputTokenAmount; + ITransformERC20Feature.Transformation[] transformations; + } + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "MetaTransactionsV2"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + /// @dev EIP712 typehash of the `MetaTransactionData` struct. + bytes32 public immutable MTX_EIP712_TYPEHASH = + keccak256( + "MetaTransactionDataV2(" + "address signer," + "address sender," + "uint256 expirationTimeSeconds," + "uint256 salt," + "bytes callData," + "address feeToken," + "MetaTransactionFeeData[] fees" + ")" + "MetaTransactionFeeData(" + "address recipient," + "uint256 amount" + ")" + ); + bytes32 public immutable MTX_FEE_TYPEHASH = + keccak256( + "MetaTransactionFeeData(" + "address recipient," + "uint256 amount" + ")" + ); + + /// @dev The WETH token contract. + IEtherToken private immutable WETH; + + /// @dev Ensures that the ETH balance of `this` does not go below the + /// initial ETH balance before the call (excluding ETH attached to the call). + modifier doesNotReduceEthBalance() { + uint256 initialBalance = address(this).balance; + _; + require(initialBalance <= address(this).balance, "MetaTransactionsFeatureV2/ETH_LEAK"); + } + + constructor(address zeroExAddress, IEtherToken weth) public FixinCommon() FixinEIP712(zeroExAddress) { + 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.executeMetaTransactionV2.selector); + _registerFeatureFunction(this.batchExecuteMetaTransactionsV2.selector); + _registerFeatureFunction(this.getMetaTransactionV2ExecutedBlock.selector); + _registerFeatureFunction(this.getMetaTransactionV2HashExecutedBlock.selector); + _registerFeatureFunction(this.getMetaTransactionV2Hash.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Execute a single meta-transaction. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnResult The ABI-encoded result of the underlying call. + function executeMetaTransactionV2( + MetaTransactionDataV2 memory mtx, + LibSignature.Signature memory signature + ) public override nonReentrant(REENTRANCY_MTX) doesNotReduceEthBalance returns (bytes memory returnResult) { + ExecuteState memory state; + state.sender = msg.sender; + state.mtx = mtx; + state.hash = getMetaTransactionV2Hash(mtx); + state.signature = signature; + + returnResult = _executeMetaTransactionPrivate(state); + } + + /// @dev Execute multiple meta-transactions. + /// @param mtxs The meta-transactions. + /// @param signatures The signature by each respective `mtx.signer`. + /// @return returnResults The ABI-encoded results of the underlying calls. + function batchExecuteMetaTransactionsV2( + MetaTransactionDataV2[] memory mtxs, + LibSignature.Signature[] memory signatures + ) public override nonReentrant(REENTRANCY_MTX) doesNotReduceEthBalance returns (bytes[] memory returnResults) { + if (mtxs.length != signatures.length) { + LibMetaTransactionsRichErrors + .InvalidMetaTransactionsArrayLengthsError(mtxs.length, signatures.length) + .rrevert(); + } + returnResults = new bytes[](mtxs.length); + for (uint256 i = 0; i < mtxs.length; ++i) { + ExecuteState memory state; + state.sender = msg.sender; + state.mtx = mtxs[i]; + state.hash = getMetaTransactionV2Hash(mtxs[i]); + state.signature = signatures[i]; + + returnResults[i] = _executeMetaTransactionPrivate(state); + } + } + + /// @dev Get the block at which a meta-transaction has been executed. + /// @param mtx The meta-transaction. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionV2ExecutedBlock( + MetaTransactionDataV2 memory mtx + ) public view override returns (uint256 blockNumber) { + return getMetaTransactionV2HashExecutedBlock(getMetaTransactionV2Hash(mtx)); + } + + /// @dev Get the block at which a meta-transaction hash has been executed. + /// @param mtxHash The meta-transaction hash. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionV2HashExecutedBlock(bytes32 mtxHash) public view override returns (uint256 blockNumber) { + return LibMetaTransactionsV2Storage.getStorage().mtxHashToExecutedBlockNumber[mtxHash]; + } + + /// @dev Get the EIP712 hash of a meta-transaction. + /// @param mtx The meta-transaction. + /// @return mtxHash The EIP712 hash of `mtx`. + function getMetaTransactionV2Hash(MetaTransactionDataV2 memory mtx) public view override returns (bytes32 mtxHash) { + bytes32[] memory feeHashes = new bytes32[](mtx.fees.length); + for (uint256 i = 0; i < mtx.fees.length; ++i) { + feeHashes[i] = keccak256(abi.encode(MTX_FEE_TYPEHASH, mtx.fees[i])); + } + + return + _getEIP712Hash( + keccak256( + abi.encode( + MTX_EIP712_TYPEHASH, + mtx.signer, + mtx.sender, + mtx.expirationTimeSeconds, + mtx.salt, + keccak256(mtx.callData), + mtx.feeToken, + keccak256(abi.encodePacked(feeHashes)) + ) + ) + ); + } + + /// @dev Execute a meta-transaction by `sender`. Low-level, hidden variant. + /// @param state The `ExecuteState` for this metatransaction, with `sender`, + /// `hash`, `mtx`, and `signature` fields filled. + /// @return returnResult The ABI-encoded result of the underlying call. + function _executeMetaTransactionPrivate(ExecuteState memory state) private returns (bytes memory returnResult) { + _validateMetaTransaction(state); + + // Mark the transaction executed by storing the block at which it was executed. + // Currently the block number just indicates that the mtx was executed and + // serves no other purpose from within this contract. + LibMetaTransactionsV2Storage.getStorage().mtxHashToExecutedBlockNumber[state.hash] = block.number; + + // Pay the fees to the fee recipients. + for (uint256 i = 0; i < state.mtx.fees.length; ++i) { + _transferERC20TokensFrom( + state.mtx.feeToken, + state.mtx.signer, + state.mtx.fees[i].recipient, + state.mtx.fees[i].amount + ); + } + + // Execute the call based on the selector. + state.selector = state.mtx.callData.readBytes4(0); + if (state.selector == ITransformERC20Feature.transformERC20.selector) { + returnResult = _executeTransformERC20Call(state); + } else if (state.selector == INativeOrdersFeature.fillLimitOrder.selector) { + returnResult = _executeFillLimitOrderCall(state); + } else if (state.selector == INativeOrdersFeature.fillRfqOrder.selector) { + returnResult = _executeFillRfqOrderCall(state); + } else if (state.selector == IMultiplexFeature.multiplexBatchSellTokenForToken.selector) { + returnResult = _executeMultiplexBatchSellTokenForTokenCall(state); + } else if (state.selector == IMultiplexFeature.multiplexBatchSellTokenForEth.selector) { + returnResult = _executeMultiplexBatchSellTokenForEthCall(state); + } else if (state.selector == IMultiplexFeature.multiplexMultiHopSellTokenForToken.selector) { + returnResult = _executeMultiplexMultiHopSellTokenForTokenCall(state); + } else if (state.selector == IMultiplexFeature.multiplexMultiHopSellTokenForEth.selector) { + returnResult = _executeMultiplexMultiHopSellTokenForEthCall(state); + } else { + LibMetaTransactionsRichErrors.MetaTransactionUnsupportedFunctionError(state.hash, state.selector).rrevert(); + } + emit MetaTransactionExecuted(state.hash, state.selector, state.mtx.signer, state.mtx.sender); + } + + /// @dev Validate that a meta-transaction is executable. + function _validateMetaTransaction(ExecuteState memory state) private view { + // Must be from the required sender, if set. + if (state.mtx.sender != address(0) && state.mtx.sender != state.sender) { + LibMetaTransactionsRichErrors + .MetaTransactionWrongSenderError(state.hash, state.sender, state.mtx.sender) + .rrevert(); + } + // Must not be expired. + if (state.mtx.expirationTimeSeconds <= block.timestamp) { + LibMetaTransactionsRichErrors + .MetaTransactionExpiredError(state.hash, block.timestamp, state.mtx.expirationTimeSeconds) + .rrevert(); + } + + if (LibSignature.getSignerOfHash(state.hash, state.signature) != state.mtx.signer) { + LibSignatureRichErrors + .SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.WRONG_SIGNER, + state.hash, + state.mtx.signer, + // TODO: Remove this field from SignatureValidationError + // when rich reverts are part of the protocol repo. + "" + ) + .rrevert(); + } + // Transaction must not have been already executed. + state.executedBlockNumber = LibMetaTransactionsV2Storage.getStorage().mtxHashToExecutedBlockNumber[state.hash]; + if (state.executedBlockNumber != 0) { + LibMetaTransactionsRichErrors + .MetaTransactionAlreadyExecutedError(state.hash, state.executedBlockNumber) + .rrevert(); + } + } + + /// @dev Execute a `ITransformERC20Feature.transformERC20()` meta-transaction call + /// by decoding the call args and translating the call to the internal + /// `ITransformERC20Feature._transformERC20()` variant, where we can override + /// the taker address. + function _executeTransformERC20Call(ExecuteState memory state) private returns (bytes memory returnResult) { + // HACK(dorothy-zbornak): `abi.decode()` with the individual args + // will cause a stack overflow. But we can prefix the call data with an + // offset to transform it into the encoding for the equivalent single struct arg, + // since decoding a single struct arg consumes far less stack space than + // decoding multiple struct args. + + // Where the encoding for multiple args (with the selector ommitted) + // would typically look like: + // | argument | offset | + // |--------------------------|---------| + // | inputToken | 0 | + // | outputToken | 32 | + // | inputTokenAmount | 64 | + // | minOutputTokenAmount | 96 | + // | transformations (offset) | 128 | = 32 + // | transformations (data) | 160 | + + // We will ABI-decode a single struct arg copy with the layout: + // | argument | offset | + // |--------------------------|---------| + // | (arg 1 offset) | 0 | = 32 + // | inputToken | 32 | + // | outputToken | 64 | + // | inputTokenAmount | 96 | + // | minOutputTokenAmount | 128 | + // | transformations (offset) | 160 | = 32 + // | transformations (data) | 192 | + + ExternalTransformERC20Args memory args; + { + bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32); + // Copy the args data from the original, after the new struct offset prefix. + bytes memory fromCallData = state.mtx.callData; + assert(fromCallData.length >= 160); + uint256 fromMem; + uint256 toMem; + assembly { + // Prefix the calldata with a struct offset, + // which points to just one word over. + mstore(add(encodedStructArgs, 32), 32) + // Copy everything after the selector. + fromMem := add(fromCallData, 36) + // Start copying after the struct offset. + toMem := add(encodedStructArgs, 64) + } + LibBytesV06.memCopy(toMem, fromMem, fromCallData.length - 4); + // Decode call args for `ITransformERC20Feature.transformERC20()` as a struct. + args = abi.decode(encodedStructArgs, (ExternalTransformERC20Args)); + } + // Call `ITransformERC20Feature._transformERC20()` (internal variant). + return + _callSelf( + state.hash, + abi.encodeWithSelector( + ITransformERC20Feature._transformERC20.selector, + ITransformERC20Feature.TransformERC20Args({ + taker: state.mtx.signer, // taker is mtx signer + inputToken: args.inputToken, + outputToken: args.outputToken, + inputTokenAmount: args.inputTokenAmount, + minOutputTokenAmount: args.minOutputTokenAmount, + transformations: args.transformations, + useSelfBalance: false, + recipient: state.mtx.signer + }) + ) + ); + } + + /// @dev Extract arguments from call data by copying everything after the + /// 4-byte selector into a new byte array. + /// @param callData The call data from which arguments are to be extracted. + /// @return args The extracted arguments as a byte array. + function _extractArgumentsFromCallData(bytes memory callData) private pure returns (bytes memory args) { + args = new bytes(callData.length - 4); + uint256 fromMem; + uint256 toMem; + + assembly { + fromMem := add(callData, 36) // skip length and 4-byte selector + toMem := add(args, 32) // write after length prefix + } + + LibBytesV06.memCopy(toMem, fromMem, args.length); + + return args; + } + + /// @dev Execute a `INativeOrdersFeature.fillLimitOrder()` meta-transaction call + /// by decoding the call args and translating the call to the internal + /// `INativeOrdersFeature._fillLimitOrder()` variant, where we can override + /// the taker address. + function _executeFillLimitOrderCall(ExecuteState memory state) private returns (bytes memory returnResult) { + LibNativeOrder.LimitOrder memory order; + LibSignature.Signature memory signature; + uint128 takerTokenFillAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (order, signature, takerTokenFillAmount) = abi.decode( + args, + (LibNativeOrder.LimitOrder, LibSignature.Signature, uint128) + ); + + return + _callSelf( + state.hash, + abi.encodeWithSelector( + INativeOrdersFeature._fillLimitOrder.selector, + order, + signature, + takerTokenFillAmount, + state.mtx.signer, // taker is mtx signer + msg.sender + ) + ); + } + + /// @dev Execute a `INativeOrdersFeature.fillRfqOrder()` meta-transaction call + /// by decoding the call args and translating the call to the internal + /// `INativeOrdersFeature._fillRfqOrder()` variant, where we can override + /// the taker address. + function _executeFillRfqOrderCall(ExecuteState memory state) private returns (bytes memory returnResult) { + LibNativeOrder.RfqOrder memory order; + LibSignature.Signature memory signature; + uint128 takerTokenFillAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (order, signature, takerTokenFillAmount) = abi.decode( + args, + (LibNativeOrder.RfqOrder, LibSignature.Signature, uint128) + ); + + return + _callSelf( + state.hash, + abi.encodeWithSelector( + INativeOrdersFeature._fillRfqOrder.selector, + order, + signature, + takerTokenFillAmount, + state.mtx.signer, // taker is mtx signer + false, + state.mtx.signer + ) + ); + } + + /// @dev Execute a `IMultiplexFeature.multiplexBatchSellTokenForToken()` meta-transaction + /// call by decoding the call args and translating the call to the internal + /// `IMultiplexFeature._multiplexBatchSell()` variant, where we can override the + /// payer address. + function _executeMultiplexBatchSellTokenForTokenCall( + ExecuteState memory state + ) private returns (bytes memory returnResult) { + IERC20Token inputToken; + IERC20Token outputToken; + IMultiplexFeature.BatchSellSubcall[] memory calls; + uint256 sellAmount; + uint256 minBuyAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (inputToken, outputToken, calls, sellAmount, minBuyAmount) = abi.decode( + args, + (IERC20Token, IERC20Token, IMultiplexFeature.BatchSellSubcall[], uint256, uint256) + ); + + return + _callSelf( + state.hash, + abi.encodeWithSelector( + IMultiplexFeature._multiplexBatchSell.selector, + IMultiplexFeature.BatchSellParams({ + inputToken: inputToken, + outputToken: outputToken, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: state.mtx.signer, + payer: state.mtx.signer + }), + minBuyAmount + ) + ); + } + + /// @dev Execute a `IMultiplexFeature.multiplexBatchSellTokenForEth()` meta-transaction + /// call by decoding the call args and translating the call to the internal + /// `IMultiplexFeature._multiplexBatchSellTokenForEth()` variant, where we can override the + /// payer address. + function _executeMultiplexBatchSellTokenForEthCall( + ExecuteState memory state + ) private returns (bytes memory returnResult) { + IERC20Token inputToken; + IMultiplexFeature.BatchSellSubcall[] memory calls; + uint256 sellAmount; + uint256 minBuyAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (inputToken, calls, sellAmount, minBuyAmount) = abi.decode( + args, + (IERC20Token, IMultiplexFeature.BatchSellSubcall[], uint256, uint256) + ); + + returnResult = _callSelf( + state.hash, + abi.encodeWithSelector( + IMultiplexFeature._multiplexBatchSell.selector, + IMultiplexFeature.BatchSellParams({ + inputToken: inputToken, + outputToken: IERC20Token(WETH), + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: address(this), + payer: state.mtx.signer + }), + minBuyAmount + ) + ); + + // Unwrap and transfer WETH + uint256 boughtAmount = abi.decode(returnResult, (uint256)); + WETH.withdraw(boughtAmount); + _transferEth(state.mtx.signer, boughtAmount); + } + + /// @dev Execute a `IMultiplexFeature.multiplexMultiHopSellTokenForToken()` meta-transaction + /// call by decoding the call args and translating the call to the internal + /// `IMultiplexFeature._multiplexMultiHopSell()` variant, where we can override the + /// payer address. + function _executeMultiplexMultiHopSellTokenForTokenCall( + ExecuteState memory state + ) private returns (bytes memory returnResult) { + address[] memory tokens; + IMultiplexFeature.MultiHopSellSubcall[] memory calls; + uint256 sellAmount; + uint256 minBuyAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (tokens, calls, sellAmount, minBuyAmount) = abi.decode( + args, + (address[], IMultiplexFeature.MultiHopSellSubcall[], uint256, uint256) + ); + + return + _callSelf( + state.hash, + abi.encodeWithSelector( + IMultiplexFeature._multiplexMultiHopSell.selector, + IMultiplexFeature.MultiHopSellParams({ + tokens: tokens, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: state.mtx.signer, + payer: state.mtx.signer + }), + minBuyAmount + ) + ); + } + + /// @dev Execute a `IMultiplexFeature.multiplexMultiHopSellTokenForEth()` meta-transaction + /// call by decoding the call args and translating the call to the internal + /// `IMultiplexFeature._multiplexMultiHopSellTokenForEth()` variant, where we can override the + /// payer address. + function _executeMultiplexMultiHopSellTokenForEthCall( + ExecuteState memory state + ) private returns (bytes memory returnResult) { + address[] memory tokens; + IMultiplexFeature.MultiHopSellSubcall[] memory calls; + uint256 sellAmount; + uint256 minBuyAmount; + + bytes memory args = _extractArgumentsFromCallData(state.mtx.callData); + (tokens, calls, sellAmount, minBuyAmount) = abi.decode( + args, + (address[], IMultiplexFeature.MultiHopSellSubcall[], uint256, uint256) + ); + + require( + tokens[tokens.length - 1] == address(WETH), + "MetaTransactionsFeature::multiplexMultiHopSellTokenForEth/NOT_WETH" + ); + + returnResult = _callSelf( + state.hash, + abi.encodeWithSelector( + IMultiplexFeature._multiplexMultiHopSell.selector, + IMultiplexFeature.MultiHopSellParams({ + tokens: tokens, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: address(this), + payer: state.mtx.signer + }), + minBuyAmount + ) + ); + + // Unwrap and transfer WETH + uint256 boughtAmount = abi.decode(returnResult, (uint256)); + WETH.withdraw(boughtAmount); + _transferEth(state.mtx.signer, boughtAmount); + } + + /// @dev Make an arbitrary internal, meta-transaction call. + /// Warning: Do not let unadulterated `callData` into this function. + function _callSelf(bytes32 hash, bytes memory callData) private returns (bytes memory returnResult) { + bool success; + (success, returnResult) = address(this).call(callData); + if (!success) { + LibMetaTransactionsRichErrors.MetaTransactionCallFailedError(hash, callData, returnResult).rrevert(); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol b/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol index 43f77b1e03..ba6f0c8d3d 100644 --- a/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol +++ b/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol @@ -70,6 +70,7 @@ contract UniswapV3Feature is IFeature, IUniswapV3Feature, FixinCommon, FixinToke _registerFeatureFunction(this.sellEthForTokenToUniswapV3.selector); _registerFeatureFunction(this.sellTokenForEthToUniswapV3.selector); _registerFeatureFunction(this.sellTokenForTokenToUniswapV3.selector); + _registerFeatureFunction(this._sellTokenForTokenToUniswapV3.selector); _registerFeatureFunction(this._sellHeldTokenForTokenToUniswapV3.selector); _registerFeatureFunction(this.uniswapV3SwapCallback.selector); return LibMigrate.MIGRATE_SUCCESS; @@ -139,6 +140,23 @@ contract UniswapV3Feature is IFeature, IUniswapV3Feature, FixinCommon, FixinToke buyAmount = _swap(encodedPath, sellAmount, minBuyAmount, msg.sender, _normalizeRecipient(recipient)); } + /// @dev Sell a token for another token directly against uniswap v3. Internal variant. + /// @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 payer. + /// @param payer The address to pull the sold tokens from. + /// @return buyAmount Amount of the last token in the path bought. + function _sellTokenForTokenToUniswapV3( + bytes memory encodedPath, + uint256 sellAmount, + uint256 minBuyAmount, + address recipient, + address payer + ) public override onlySelf returns (uint256 buyAmount) { + buyAmount = _swap(encodedPath, sellAmount, minBuyAmount, payer, _normalizeRecipient(recipient, payer)); + } + /// @dev Sell a token for another token directly against uniswap v3. /// Private variant, uses tokens held by `address(this)`. /// @param encodedPath Uniswap-encoded path. @@ -337,8 +355,16 @@ contract UniswapV3Feature is IFeature, IUniswapV3Feature, FixinCommon, FixinToke } } + // Convert null address values to alternative address. + function _normalizeRecipient( + address recipient, + address alternative + ) private pure returns (address payable normalizedRecipient) { + return recipient == address(0) ? payable(alternative) : payable(recipient); + } + // Convert null address values to msg.sender. function _normalizeRecipient(address recipient) private view returns (address payable normalizedRecipient) { - return recipient == address(0) ? msg.sender : payable(recipient); + return _normalizeRecipient(recipient, msg.sender); } } diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol b/contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol new file mode 100644 index 0000000000..511cf3f638 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + Copyright 2023 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/src/IERC20Token.sol"; +import "../libs/LibSignature.sol"; + +/// @dev Meta-transactions feature. +interface IMetaTransactionsFeatureV2 { + /// @dev Describes an exchange proxy meta transaction. + struct MetaTransactionFeeData { + // ERC20 fee recipient + address recipient; + // ERC20 fee amount + uint256 amount; + } + + struct MetaTransactionDataV2 { + // Signer of meta-transaction. On whose behalf to execute the MTX. + address payable signer; + // Required sender, or NULL for anyone. + address sender; + // MTX is invalid after this time. + uint256 expirationTimeSeconds; + // Nonce to make this MTX unique. + uint256 salt; + // Encoded call data to a function on the exchange proxy. + bytes callData; + // ERC20 fee `signer` pays `sender`. + IERC20Token feeToken; + // ERC20 fees. + MetaTransactionFeeData[] fees; + } + + /// @dev Emitted whenever a meta-transaction is executed via + /// `executeMetaTransaction()` or `executeMetaTransactions()`. + /// @param hash The EIP712 hash of the MetaTransactionDataV2 struct. + /// @param selector The selector of the function being executed. + /// @param signer Who to execute the meta-transaction on behalf of. + /// @param sender Who executed the meta-transaction. + event MetaTransactionExecuted(bytes32 hash, bytes4 indexed selector, address signer, address sender); + + /// @dev Execute a single meta-transaction. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnResult The ABI-encoded result of the underlying call. + function executeMetaTransactionV2( + MetaTransactionDataV2 calldata mtx, + LibSignature.Signature calldata signature + ) external returns (bytes memory returnResult); + + /// @dev Execute multiple meta-transactions. + /// @param mtxs The meta-transactions. + /// @param signatures The signature by each respective `mtx.signer`. + /// @return returnResults The ABI-encoded results of the underlying calls. + function batchExecuteMetaTransactionsV2( + MetaTransactionDataV2[] calldata mtxs, + LibSignature.Signature[] calldata signatures + ) external returns (bytes[] memory returnResults); + + /// @dev Get the block at which a meta-transaction has been executed. + /// @param mtx The meta-transaction. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionV2ExecutedBlock( + MetaTransactionDataV2 calldata mtx + ) external view returns (uint256 blockNumber); + + /// @dev Get the block at which a meta-transaction hash has been executed. + /// @param mtxHash The EIP712 hash of the MetaTransactionDataV2 struct. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionV2HashExecutedBlock(bytes32 mtxHash) external view returns (uint256 blockNumber); + + /// @dev Get the EIP712 hash of a meta-transaction. + /// @param mtx The meta-transaction. + /// @return mtxHash The EIP712 hash of `mtx`. + function getMetaTransactionV2Hash(MetaTransactionDataV2 calldata mtx) external view returns (bytes32 mtxHash); +} diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol index eecf260287..ce05d047f9 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol @@ -46,6 +46,8 @@ interface IMultiplexFeature { bool useSelfBalance; // The recipient of the bought output tokens. address recipient; + // The sender of the input tokens. + address payer; } // Represents a constituent call of a batch sell. @@ -75,6 +77,8 @@ interface IMultiplexFeature { bool useSelfBalance; // The recipient of the bought output tokens. address recipient; + // The sender of the input tokens. + address payer; } // Represents a constituent call of a multi-hop sell. @@ -153,6 +157,17 @@ interface IMultiplexFeature { uint256 minBuyAmount ) external returns (uint256 boughtAmount); + /// @dev Executes a multiplex BatchSell using the given + /// parameters. Internal only. + /// @param params The parameters for the BatchSell. + /// @param minBuyAmount The minimum amount of `params.outputToken` + /// that must be bought for this function to not revert. + /// @return boughtAmount The amount of `params.outputToken` bought. + function _multiplexBatchSell( + BatchSellParams memory params, + 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 @@ -204,4 +219,15 @@ interface IMultiplexFeature { uint256 sellAmount, uint256 minBuyAmount ) external returns (uint256 boughtAmount); + + /// @dev Executes a multiplex MultiHopSell using the given + /// parameters. Internal only. + /// @param params The parameters for the MultiHopSell. + /// @param minBuyAmount The minimum amount of the output token + /// that must be bought for this function to not revert. + /// @return boughtAmount The amount of the output token bought. + function _multiplexMultiHopSell( + MultiHopSellParams memory params, + uint256 minBuyAmount + ) external returns (uint256 boughtAmount); } diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol index 89588d7fd3..21105a1fda 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol @@ -54,6 +54,21 @@ interface IUniswapV3Feature { address recipient ) external returns (uint256 buyAmount); + /// @dev Sell a token for another token directly against uniswap v3. Internal variant. + /// @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 payer. + /// @param payer The address to pull the sold tokens from. + /// @return buyAmount Amount of the last token in the path bought. + function _sellTokenForTokenToUniswapV3( + bytes memory encodedPath, + uint256 sellAmount, + uint256 minBuyAmount, + address recipient, + address payer + ) 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. diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol index ca6481602a..784f2178e7 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol @@ -80,9 +80,11 @@ contract MultiplexFeature is _registerFeatureFunction(this.multiplexBatchSellEthForToken.selector); _registerFeatureFunction(this.multiplexBatchSellTokenForEth.selector); _registerFeatureFunction(this.multiplexBatchSellTokenForToken.selector); + _registerFeatureFunction(this._multiplexBatchSell.selector); _registerFeatureFunction(this.multiplexMultiHopSellEthForToken.selector); _registerFeatureFunction(this.multiplexMultiHopSellTokenForEth.selector); _registerFeatureFunction(this.multiplexMultiHopSellTokenForToken.selector); + _registerFeatureFunction(this._multiplexMultiHopSell.selector); return LibMigrate.MIGRATE_SUCCESS; } @@ -103,14 +105,15 @@ contract MultiplexFeature is // WETH is now held by this contract, // so `useSelfBalance` is true. return - _multiplexBatchSell( + _multiplexBatchSellPrivate( BatchSellParams({ inputToken: WETH, outputToken: outputToken, sellAmount: msg.value, calls: calls, useSelfBalance: true, - recipient: msg.sender + recipient: msg.sender, + payer: msg.sender }), minBuyAmount ); @@ -133,14 +136,15 @@ contract MultiplexFeature is // 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( + boughtAmount = _multiplexBatchSellPrivate( BatchSellParams({ inputToken: inputToken, outputToken: WETH, sellAmount: sellAmount, calls: calls, useSelfBalance: false, - recipient: address(this) + recipient: address(this), + payer: msg.sender }), minBuyAmount ); @@ -167,26 +171,40 @@ contract MultiplexFeature is uint256 minBuyAmount ) public override returns (uint256 boughtAmount) { return - _multiplexBatchSell( + _multiplexBatchSellPrivate( BatchSellParams({ inputToken: inputToken, outputToken: outputToken, sellAmount: sellAmount, calls: calls, useSelfBalance: false, - recipient: msg.sender + recipient: msg.sender, + payer: msg.sender }), minBuyAmount ); } + /// @dev Executes a batch sell and checks that at least + /// `minBuyAmount` of `outputToken` was bought. Internal variant. + /// @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 + ) public override onlySelf returns (uint256 boughtAmount) { + return _multiplexBatchSellPrivate(params, 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( + function _multiplexBatchSellPrivate( BatchSellParams memory params, uint256 minBuyAmount ) private returns (uint256 boughtAmount) { @@ -226,13 +244,14 @@ contract MultiplexFeature is // WETH is now held by this contract, // so `useSelfBalance` is true. return - _multiplexMultiHopSell( + _multiplexMultiHopSellPrivate( MultiHopSellParams({ tokens: tokens, sellAmount: msg.value, calls: calls, useSelfBalance: true, - recipient: msg.sender + recipient: msg.sender, + payer: msg.sender }), minBuyAmount ); @@ -262,13 +281,14 @@ contract MultiplexFeature is ); // The `recipient of the WETH is set to this contract, since // we must unwrap the WETH and transfer the resulting ETH. - boughtAmount = _multiplexMultiHopSell( + boughtAmount = _multiplexMultiHopSellPrivate( MultiHopSellParams({ tokens: tokens, sellAmount: sellAmount, calls: calls, useSelfBalance: false, - recipient: address(this) + recipient: address(this), + payer: msg.sender }), minBuyAmount ); @@ -297,25 +317,38 @@ contract MultiplexFeature is uint256 minBuyAmount ) public override returns (uint256 boughtAmount) { return - _multiplexMultiHopSell( + _multiplexMultiHopSellPrivate( MultiHopSellParams({ tokens: tokens, sellAmount: sellAmount, calls: calls, useSelfBalance: false, - recipient: msg.sender + recipient: msg.sender, + payer: msg.sender }), minBuyAmount ); } + /// @dev Executes a multi-hop sell. Internal variant. + /// @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 + ) public override onlySelf returns (uint256 boughtAmount) { + return _multiplexMultiHopSellPrivate(params, 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( + function _multiplexMultiHopSellPrivate( MultiHopSellParams memory params, uint256 minBuyAmount ) private returns (uint256 boughtAmount) { @@ -387,14 +420,14 @@ contract MultiplexFeature is // 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. + // `payer`, `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 + // If the input tokens are currently held by `payer` but // the first hop expects them elsewhere, perform a `transferFrom`. - if (!params.useSelfBalance && state.from != msg.sender) { - _transferERC20TokensFrom(IERC20Token(params.tokens[0]), msg.sender, state.from, params.sellAmount); + if (!params.useSelfBalance && state.from != params.payer) { + _transferERC20TokensFrom(IERC20Token(params.tokens[0]), params.payer, state.from, params.sellAmount); } // If the input tokens are currently held by `address(this)` but // the first hop expects them elsewhere, perform a `transfer`. @@ -411,11 +444,13 @@ contract MultiplexFeature is if (subcall.id == MultiplexSubcall.UniswapV2) { _multiHopSellUniswapV2(state, params, subcall.data); } else if (subcall.id == MultiplexSubcall.UniswapV3) { - _multiHopSellUniswapV3(state, subcall.data); + _multiHopSellUniswapV3(state, params, 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 if (subcall.id == MultiplexSubcall.OTC) { + _multiHopSellOtcOrder(state, params, subcall.data); } else { revert("MultiplexFeature::_executeMultiHopSell/INVALID_SUBCALL"); } @@ -443,6 +478,8 @@ contract MultiplexFeature is // Likewise, the recipient of the multi-hop sell is // equal to the recipient of its containing batch sell. multiHopParams.recipient = params.recipient; + // The payer is the same too. + multiHopParams.payer = params.payer; // Execute the nested multi-hop sell. uint256 outputTokenAmount = _executeMultiHopSell(multiHopParams).outputTokenAmount; // Increment the sold and bought amounts. @@ -469,7 +506,7 @@ contract MultiplexFeature is // 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` + // pull tokens from `payer` (so `batchSellParams.useSelfBalance` // should be false). Otherwise `batchSellParams.useSelfBalance` // should be true. batchSellParams.useSelfBalance = state.hopIndex > 0 || params.useSelfBalance; @@ -477,6 +514,8 @@ contract MultiplexFeature is // that should receive the output tokens of the // batch sell. batchSellParams.recipient = state.to; + // payer shound be the same too. + batchSellParams.payer = params.payer; // Execute the nested batch sell. state.outputTokenAmount = _executeBatchSell(batchSellParams).boughtAmount; } @@ -505,29 +544,33 @@ contract MultiplexFeature is // 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) { + } else if ( + subcall.id == MultiplexSubcall.UniswapV3 || + subcall.id == MultiplexSubcall.BatchSell || + subcall.id == MultiplexSubcall.OTC + ) { // 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 + // - call `transferFrom` to move tokens from `payer` 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 from `payer`, 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`, + // path. The input tokens are either held by `payer`, // 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 + // which `payer` 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; + target = params.payer; } else { target = address(this); } diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol index c880750f19..fddc797d0c 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol @@ -67,7 +67,7 @@ abstract contract MultiplexLiquidityProvider is FixinCommon, FixinTokenSpender { _transferERC20Tokens(params.inputToken, provider, sellAmount); } else { // Otherwise, transfer the input tokens from `msg.sender`. - _transferERC20TokensFrom(params.inputToken, msg.sender, provider, sellAmount); + _transferERC20TokensFrom(params.inputToken, params.payer, provider, sellAmount); } // Cache the recipient's balance of the output token. uint256 balanceBefore = params.outputToken.balanceOf(params.recipient); diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol index 49fd673efa..9884ff0f66 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol @@ -55,7 +55,7 @@ abstract contract MultiplexOtc is FixinEIP712 { order, signature, sellAmount.safeDowncastToUint128(), - msg.sender, + params.payer, params.useSelfBalance, params.recipient ) @@ -65,4 +65,34 @@ abstract contract MultiplexOtc is FixinEIP712 { state.boughtAmount = state.boughtAmount.safeAdd(makerTokenFilledAmount); } catch {} } + + function _multiHopSellOtcOrder( + IMultiplexFeature.MultiHopSellState memory state, + IMultiplexFeature.MultiHopSellParams memory params, + bytes memory wrappedCallData + ) internal { + // Decode the Otc order, and signature. + (LibNativeOrder.OtcOrder memory order, LibSignature.Signature memory signature) = abi.decode( + wrappedCallData, + (LibNativeOrder.OtcOrder, LibSignature.Signature) + ); + //Make sure that the otc orders maker and taker tokens match the fill sequence in params.tokens[] + require( + address(order.takerToken) == params.tokens[state.hopIndex] && + address(order.makerToken) == params.tokens[state.hopIndex + 1], + "MultiplexOtcOrder::_multiHopSellOtcOrder/INVALID_TOKENS" + ); + // Try filling the Otc order. Bubble up reverts. + (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) = IOtcOrdersFeature(address(this)) + ._fillOtcOrder( + order, + signature, + state.outputTokenAmount.safeDowncastToUint128(), + state.from, + params.useSelfBalance, + state.to + ); + //store the bought amount for the next hop + state.outputTokenAmount = makerTokenFilledAmount; + } } diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol index 633e9e4efb..d76ab6cfbc 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol @@ -54,7 +54,7 @@ abstract contract MultiplexRfq is FixinEIP712 { order, signature, sellAmount.safeDowncastToUint128(), - msg.sender, + params.payer, params.useSelfBalance, params.recipient ) diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol index 289401e175..e8ea431d6a 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol @@ -30,8 +30,8 @@ abstract contract MultiplexTransformERC20 { ) internal { ITransformERC20Feature.TransformERC20Args memory args; // We want the TransformedERC20 event to have - // `msg.sender` as the taker. - args.taker = msg.sender; + // `payer` as the taker. + args.taker = payable(params.payer); args.inputToken = params.inputToken; args.outputToken = params.outputToken; args.inputTokenAmount = sellAmount; diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol index 6582e40b0b..cbd588873c 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol @@ -77,7 +77,7 @@ abstract contract MultiplexUniswapV2 is FixinCommon, FixinTokenSpender { if (params.useSelfBalance) { _transferERC20Tokens(IERC20Token(tokens[0]), firstPairAddress, sellAmount); } else { - _transferERC20TokensFrom(IERC20Token(tokens[0]), msg.sender, firstPairAddress, sellAmount); + _transferERC20TokensFrom(IERC20Token(tokens[0]), params.payer, firstPairAddress, sellAmount); } // Execute the Uniswap/Sushiswap trade. return _sellToUniswapV2(tokens, sellAmount, isSushi, firstPairAddress, params.recipient); diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol index 4b34dfd32b..1b636b22d9 100644 --- a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol @@ -45,16 +45,16 @@ abstract contract MultiplexUniswapV3 is FixinTokenSpender { ) ); } else { - // Otherwise, we self-delegatecall the normal variant - // `sellTokenForTokenToUniswapV3`, which pulls the input token - // from `msg.sender`. - (success, resultData) = address(this).delegatecall( + // Otherwise, we self-call `_sellTokenForTokenToUniswapV3`, + // which pulls the input token from a specified `payer`. + (success, resultData) = address(this).call( abi.encodeWithSelector( - IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector, + IUniswapV3Feature._sellTokenForTokenToUniswapV3.selector, wrappedCallData, sellAmount, 0, - params.recipient + params.recipient, + params.payer ) ); } @@ -69,6 +69,7 @@ abstract contract MultiplexUniswapV3 is FixinTokenSpender { function _multiHopSellUniswapV3( IMultiplexFeature.MultiHopSellState memory state, + IMultiplexFeature.MultiHopSellParams memory params, bytes memory wrappedCallData ) internal { bool success; @@ -87,16 +88,16 @@ abstract contract MultiplexUniswapV3 is FixinTokenSpender { ) ); } else { - // Otherwise, we self-delegatecall the normal variant - // `sellTokenForTokenToUniswapV3`, which pulls the input token - // from `msg.sender`. - (success, resultData) = address(this).delegatecall( + // Otherwise, we self-call `_sellTokenForTokenToUniswapV3`, + // which pulls the input token from `payer`. + (success, resultData) = address(this).call( abi.encodeWithSelector( - IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector, + IUniswapV3Feature._sellTokenForTokenToUniswapV3.selector, wrappedCallData, state.outputTokenAmount, 0, - state.to + state.to, + params.payer ) ); } diff --git a/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsV2Storage.sol b/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsV2Storage.sol new file mode 100644 index 0000000000..88644d381a --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsV2Storage.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + Copyright 2023 ZeroEx Intl. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "./LibStorage.sol"; + +/// @dev Storage helpers for the `MetaTransactions` feature. +library LibMetaTransactionsV2Storage { + /// @dev Storage bucket for this feature. + struct Storage { + // The block number when a hash was executed. + mapping(bytes32 => uint256) mtxHashToExecutedBlockNumber; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageSlot = LibStorage.getStorageSlot(LibStorage.StorageId.MetaTransactionsV2); + // Dip into assembly to change the slot pointed to by the local variable `stor`. + // solhint-disable-next-line max-line-length + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { + stor_slot := storageSlot + } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibStorage.sol b/contracts/zero-ex/contracts/src/storage/LibStorage.sol index 4d758e20ab..1603946113 100644 --- a/contracts/zero-ex/contracts/src/storage/LibStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibStorage.sol @@ -34,7 +34,8 @@ library LibStorage { NativeOrders, OtcOrders, ERC721Orders, - ERC1155Orders + ERC1155Orders, + MetaTransactionsV2 } /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced slots to storage bucket variables diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 26cfe26751..2241990cda 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -36,9 +36,9 @@ "typechain": "typechain --target=ethers-v5 --out-dir='typechain-wrappers' './foundry-artifacts/**/*.json'" }, "config": { - "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature,AvalancheBridgeAdapter,BSCBridgeAdapter,CeloBridgeAdapter,EthereumBridgeAdapter,FantomBridgeAdapter,OptimismBridgeAdapter,PolygonBridgeAdapter", + "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature,AvalancheBridgeAdapter,BSCBridgeAdapter,CeloBridgeAdapter,EthereumBridgeAdapter,FantomBridgeAdapter,OptimismBridgeAdapter,PolygonBridgeAdapter,MetaTransactionsFeatureV2", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AbstractBridgeAdapter|AffiliateFeeTransformer|ArbitrumBridgeAdapter|AvalancheBridgeAdapter|BSCBridgeAdapter|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeProtocols|CeloBridgeAdapter|CurveLiquidityProvider|ERC1155OrdersFeature|ERC165Feature|ERC721OrdersFeature|EthereumBridgeAdapter|FantomBridgeAdapter|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinERC1155Spender|FixinERC721Spender|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|FundRecoveryFeature|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC1155OrdersFeature|IERC1155Token|IERC165Feature|IERC20Bridge|IERC20Transformer|IERC721OrdersFeature|IERC721Token|IFeature|IFeeRecipient|IFlashWallet|IFundRecoveryFeature|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|IPropertyValidator|ISimpleFunctionRegistryFeature|IStaking|ITakerCallback|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC1155OrdersStorage|LibERC20Transformer|LibERC721OrdersStorage|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNFTOrder|LibNFTOrdersRichErrors|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAaveV2|MixinBalancer|MixinBalancerV2Batch|MixinBancor|MixinBancorV3|MixinCompound|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinGMX|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinPlatypus|MixinShell|MixinSolidly|MixinSynthetix|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NFTOrders|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OptimismBridgeAdapter|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PolygonBridgeAdapter|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFeeRecipient|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC1155Token|TestMintableERC20Token|TestMintableERC721Token|TestMooniswap|TestNFTOrderPresigner|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestPropertyValidator|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AbstractBridgeAdapter|AffiliateFeeTransformer|ArbitrumBridgeAdapter|AvalancheBridgeAdapter|BSCBridgeAdapter|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeProtocols|CeloBridgeAdapter|CurveLiquidityProvider|ERC1155OrdersFeature|ERC165Feature|ERC721OrdersFeature|EthereumBridgeAdapter|FantomBridgeAdapter|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinERC1155Spender|FixinERC721Spender|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|FundRecoveryFeature|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC1155OrdersFeature|IERC1155Token|IERC165Feature|IERC20Bridge|IERC20Transformer|IERC721OrdersFeature|IERC721Token|IFeature|IFeeRecipient|IFlashWallet|IFundRecoveryFeature|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMetaTransactionsFeatureV2|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|IPropertyValidator|ISimpleFunctionRegistryFeature|IStaking|ITakerCallback|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC1155OrdersStorage|LibERC20Transformer|LibERC721OrdersStorage|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNFTOrder|LibNFTOrdersRichErrors|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MetaTransactionsFeatureV2|MixinAaveV2|MixinBalancer|MixinBalancerV2Batch|MixinBancor|MixinBancorV3|MixinCompound|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinGMX|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinPlatypus|MixinShell|MixinSolidly|MixinSynthetix|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NFTOrders|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OptimismBridgeAdapter|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PolygonBridgeAdapter|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFeeRecipient|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC1155Token|TestMintableERC20Token|TestMintableERC721Token|TestMooniswap|TestNFTOrderPresigner|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestPropertyValidator|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index b395f864a0..654429c0bc 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -32,6 +32,7 @@ import * as IZeroEx from '../generated-artifacts/IZeroEx.json'; import * as LiquidityProviderFeature from '../generated-artifacts/LiquidityProviderFeature.json'; import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json'; +import * as MetaTransactionsFeatureV2 from '../generated-artifacts/MetaTransactionsFeatureV2.json'; import * as MultiplexFeature from '../generated-artifacts/MultiplexFeature.json'; import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; import * as OptimismBridgeAdapter from '../generated-artifacts/OptimismBridgeAdapter.json'; @@ -84,4 +85,5 @@ export const artifacts = { FantomBridgeAdapter: FantomBridgeAdapter as ContractArtifact, OptimismBridgeAdapter: OptimismBridgeAdapter as ContractArtifact, PolygonBridgeAdapter: PolygonBridgeAdapter as ContractArtifact, + MetaTransactionsFeatureV2: MetaTransactionsFeatureV2 as ContractArtifact, }; diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index fbb608d91a..785fbe514b 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -30,6 +30,7 @@ export * from '../generated-wrappers/initial_migration'; export * from '../generated-wrappers/liquidity_provider_feature'; export * from '../generated-wrappers/log_metadata_transformer'; export * from '../generated-wrappers/meta_transactions_feature'; +export * from '../generated-wrappers/meta_transactions_feature_v2'; export * from '../generated-wrappers/multiplex_feature'; export * from '../generated-wrappers/native_orders_feature'; export * from '../generated-wrappers/optimism_bridge_adapter'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 16b24c9da8..adafc829c2 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -51,6 +51,7 @@ import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvi import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json'; import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; +import * as IMetaTransactionsFeatureV2 from '../test/generated-artifacts/IMetaTransactionsFeatureV2.json'; import * as IMooniswapPool from '../test/generated-artifacts/IMooniswapPool.json'; import * as IMultiplexFeature from '../test/generated-artifacts/IMultiplexFeature.json'; import * as INativeOrdersEvents from '../test/generated-artifacts/INativeOrdersEvents.json'; @@ -104,6 +105,7 @@ import * as LiquidityProviderFeature from '../test/generated-artifacts/Liquidity import * as LiquidityProviderSandbox from '../test/generated-artifacts/LiquidityProviderSandbox.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; +import * as MetaTransactionsFeatureV2 from '../test/generated-artifacts/MetaTransactionsFeatureV2.json'; import * as MixinAaveV2 from '../test/generated-artifacts/MixinAaveV2.json'; import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json'; import * as MixinBalancerV2Batch from '../test/generated-artifacts/MixinBalancerV2Batch.json'; @@ -236,6 +238,7 @@ export const artifacts = { FundRecoveryFeature: FundRecoveryFeature as ContractArtifact, LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, + MetaTransactionsFeatureV2: MetaTransactionsFeatureV2 as ContractArtifact, NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, OtcOrdersFeature: OtcOrdersFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, @@ -253,6 +256,7 @@ export const artifacts = { IFundRecoveryFeature: IFundRecoveryFeature as ContractArtifact, ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, + IMetaTransactionsFeatureV2: IMetaTransactionsFeatureV2 as ContractArtifact, IMultiplexFeature: IMultiplexFeature as ContractArtifact, INativeOrdersEvents: INativeOrdersEvents as ContractArtifact, INativeOrdersFeature: INativeOrdersFeature as ContractArtifact, diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 5efc29b73b..7f703bc267 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -49,6 +49,7 @@ export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_feature'; export * from '../test/generated-wrappers/i_liquidity_provider_sandbox'; export * from '../test/generated-wrappers/i_meta_transactions_feature'; +export * from '../test/generated-wrappers/i_meta_transactions_feature_v2'; export * from '../test/generated-wrappers/i_mooniswap_pool'; export * from '../test/generated-wrappers/i_multiplex_feature'; export * from '../test/generated-wrappers/i_native_orders_events'; @@ -102,6 +103,7 @@ export * from '../test/generated-wrappers/liquidity_provider_feature'; export * from '../test/generated-wrappers/liquidity_provider_sandbox'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; +export * from '../test/generated-wrappers/meta_transactions_feature_v2'; export * from '../test/generated-wrappers/mixin_aave_v2'; export * from '../test/generated-wrappers/mixin_balancer'; export * from '../test/generated-wrappers/mixin_balancer_v2_batch'; diff --git a/contracts/zero-ex/tests/MetaTransactionV2Test.t.sol b/contracts/zero-ex/tests/MetaTransactionV2Test.t.sol new file mode 100644 index 0000000000..0942d49151 --- /dev/null +++ b/contracts/zero-ex/tests/MetaTransactionV2Test.t.sol @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + Copyright 2023 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 "./utils/BaseTest.sol"; +import "forge-std/Test.sol"; +import "./utils/LocalTest.sol"; +import "../contracts/src/features/MetaTransactionsFeatureV2.sol"; +import "../contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol"; +import "../contracts/src/features/interfaces/IMetaTransactionsFeature.sol"; +import "../contracts/test/TestMintTokenERC20Transformer.sol"; +import "../contracts/src/features/libs/LibSignature.sol"; +import "src/features/libs/LibNativeOrder.sol"; +import "../contracts/test/tokens/TestMintableERC20Token.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/src/IEtherToken.sol"; + +contract MetaTransactionTest is LocalTest { + address private constant USER_ADDRESS = 0x6dc3a54FeAE57B65d185A7B159c5d3FA7fD7FD0F; + uint256 private constant USER_KEY = 0x1fc1630343b31e60b7a197a53149ca571ed9d9791e2833337bbd8110c30710ec; + + event MetaTransactionExecuted(bytes32 hash, bytes4 indexed selector, address signer, address sender); + + function _mtxSignature( + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx + ) private returns (LibSignature.Signature memory) { + return _mtxSignatureWithSignerKey(mtx, USER_KEY); + } + + function _mtxSignatureWithSignerKey( + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx, + uint256 key + ) private returns (LibSignature.Signature memory) { + // Mint fee to signer and approve + for (uint256 i = 0; i < mtx.fees.length; ++i) { + _mintTo(address(weth), mtx.signer, mtx.fees[i].amount); + } + vm.prank(mtx.signer); + mtx.feeToken.approve(address(zeroExDeployed.zeroEx), 1e18); + + bytes32 mtxHash = zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtx); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, mtxHash); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + return sig; + } + + function _getMetaTransaction( + bytes memory callData + ) private view returns (IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory) { + IMetaTransactionsFeatureV2.MetaTransactionFeeData[] + memory fees = new IMetaTransactionsFeatureV2.MetaTransactionFeeData[](1); + fees[0] = IMetaTransactionsFeatureV2.MetaTransactionFeeData({recipient: address(this), amount: 1}); + return _getMetaTransactionWithFees(callData, fees); + } + + function _getMetaTransactionWithFees( + bytes memory callData, + IMetaTransactionsFeatureV2.MetaTransactionFeeData[] memory fees + ) private view returns (IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory) { + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx = IMetaTransactionsFeatureV2.MetaTransactionDataV2({ + signer: payable(USER_ADDRESS), + sender: address(this), + expirationTimeSeconds: block.timestamp + 60, + salt: 123, + callData: callData, + feeToken: weth, + fees: fees + }); + return mtx; + } + + function _badSelectorTransformERC20Call() private pure returns (bytes memory) { + return abi.encodeWithSelector(ITransformERC20Feature.createTransformWallet.selector); + } + + function _badTokenTransformERC20Call() private returns (bytes memory) { + ITransformERC20Feature.Transformation[] memory transformations = new ITransformERC20Feature.Transformation[](1); + transformations[0] = ITransformERC20Feature.Transformation( + uint32(transformerNonce), + abi.encode(address(dai), address(weth), 0, 1e18, 0) + ); + + _mintTo(address(dai), USER_ADDRESS, 1e18); + vm.prank(USER_ADDRESS); + dai.approve(address(zeroExDeployed.zeroEx), 1e18); + + return + abi.encodeWithSelector( + zeroExDeployed.zeroEx.transformERC20.selector, // 0x415565b0 + dai, + weth, + 1e18, + 1e18, + transformations + ); + } + + function _makeTestRfqOrder( + IERC20Token makerToken, + IERC20Token takerToken, + address makerAddress, + address takerAddress, + uint256 makerKey + ) internal returns (bytes memory callData) { + LibNativeOrder.RfqOrder memory order = LibNativeOrder.RfqOrder({ + makerToken: makerToken, + takerToken: takerToken, + makerAmount: 1e18, + takerAmount: 1e18, + maker: makerAddress, + taker: address(0), + txOrigin: tx.origin, + pool: 0x0000000000000000000000000000000000000000000000000000000000000000, + expiry: uint64(block.timestamp + 60), + salt: 123 + }); + _mintTo(address(order.makerToken), order.maker, order.makerAmount); + vm.prank(order.maker); + order.makerToken.approve(address(zeroExDeployed.zeroEx), order.makerAmount); + _mintTo(address(order.takerToken), takerAddress, order.takerAmount); + vm.prank(takerAddress); + order.takerToken.approve(address(zeroExDeployed.zeroEx), order.takerAmount); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + makerKey, + zeroExDeployed.features.nativeOrdersFeature.getRfqOrderHash(order) + ); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + return + abi.encodeWithSelector( + INativeOrdersFeature.fillRfqOrder.selector, // 0xaa77476c + order, // RFQOrder + sig, // Order Signature + 1e18 // Fill Amount + ); + } + + function _makeTestLimitOrder( + IERC20Token makerToken, + IERC20Token takerToken, + address makerAddress, + address takerAddress, + uint256 makerKey + ) internal returns (bytes memory callData) { + LibNativeOrder.LimitOrder memory order = LibNativeOrder.LimitOrder({ + makerToken: makerToken, + takerToken: takerToken, + makerAmount: 1e18, + takerAmount: 1e18, + maker: makerAddress, + taker: address(0), + sender: address(0), + takerTokenFeeAmount: 0, + feeRecipient: address(0), + pool: 0x0000000000000000000000000000000000000000000000000000000000000000, + expiry: uint64(block.timestamp + 60), + salt: 123 + }); + _mintTo(address(order.makerToken), order.maker, order.makerAmount); + vm.prank(order.maker); + order.makerToken.approve(address(zeroExDeployed.zeroEx), order.makerAmount); + _mintTo(address(order.takerToken), takerAddress, order.takerAmount); + vm.prank(takerAddress); + order.takerToken.approve(address(zeroExDeployed.zeroEx), order.takerAmount); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + makerKey, + zeroExDeployed.features.nativeOrdersFeature.getLimitOrderHash(order) + ); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + return + abi.encodeWithSelector( + INativeOrdersFeature.fillLimitOrder.selector, // 0xf6274f66 + order, // LimitOrder + sig, // Order Signature + 1e18 // Fill Amount + ); + } + + function _transformERC20Call( + IERC20Token makerToken, + IERC20Token takerToken, + address takerAddress + ) internal returns (bytes memory) { + ITransformERC20Feature.Transformation[] memory transformations = new ITransformERC20Feature.Transformation[](1); + transformations[0] = ITransformERC20Feature.Transformation( + uint32(transformerNonce), + abi.encode(address(takerToken), address(makerToken), 0, 1e18, 0) + ); + + _mintTo(address(takerToken), takerAddress, 1e18); + vm.prank(takerAddress); + takerToken.approve(address(zeroExDeployed.zeroEx), 1e18); + + return + abi.encodeWithSelector( + zeroExDeployed.zeroEx.transformERC20.selector, // 0x415565b0 + takerToken, + makerToken, + 1e18, + 1e18, + transformations + ); + } + + function test_createHash() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + + bytes32 mtxHash = zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtxData); + assertTrue(mtxHash != bytes32(0)); + } + + function test_EIP_712_signature() external { + // metamask wallet signed data + bytes32 r_mm = 0xcd6c09d558e23803afae870ca53a8e7bfaf5564c64ee29f23dc4a19e7dd9e9b5; + bytes32 s_mm = 0x1ae68e89fadab4a7f4d01fd5543e5e0efd5697e87c993f045f671aba3e1f55ac; + uint8 v_mm = 0x1b; + + IMetaTransactionsFeatureV2.MetaTransactionFeeData[] + memory fees = new IMetaTransactionsFeatureV2.MetaTransactionFeeData[](2); + fees[0] = IMetaTransactionsFeatureV2.MetaTransactionFeeData({recipient: address(0), amount: 1000000}); + fees[1] = IMetaTransactionsFeatureV2.MetaTransactionFeeData({recipient: address(0), amount: 1000}); + IERC20Token usdcToken = IERC20Token(address(0x2e234DAe75C793f67A35089C9d99245E1C58470b)); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx = IMetaTransactionsFeatureV2.MetaTransactionDataV2({ + signer: address(0), + sender: address(0), + expirationTimeSeconds: 99999999, + salt: 1234, + callData: new bytes(0), + feeToken: usdcToken, + fees: fees + }); + + bytes32 mtxHash = zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtx); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(USER_KEY, mtxHash); + //emit log_bytes(abi.encodePacked(r, s, bytes1(v))); + + // Verify signature matches from what we generated using metamask + assertTrue(v == v_mm); + assertTrue(r == r_mm); + assertTrue(s == s_mm); + } + + function test_transformERC20() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + vm.expectEmit(true, false, false, true); + emit MetaTransactionExecuted( + zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtxData), + zeroExDeployed.zeroEx.transformERC20.selector, // 0x415565b0 + USER_ADDRESS, + address(this) + ); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(weth.balanceOf(address(this)), 1); + } + + function test_rfqOrder() external { + bytes memory callData = _makeTestRfqOrder(zrx, dai, signerAddress, USER_ADDRESS, signerKey); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(callData); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + vm.expectEmit(true, false, false, true); + emit MetaTransactionExecuted( + zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtxData), + INativeOrdersFeature.fillRfqOrder.selector, // 0xaa77476c + USER_ADDRESS, + address(this) + ); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + + assertEq(zrx.balanceOf(signerAddress), 0); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(dai.balanceOf(signerAddress), 1e18); + assertEq(weth.balanceOf(address(this)), 1); + } + + function test_fillLimitOrder() external { + bytes memory callData = _makeTestLimitOrder(zrx, dai, signerAddress, USER_ADDRESS, signerKey); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(callData); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + vm.expectEmit(true, false, false, true); + emit MetaTransactionExecuted( + zeroExDeployed.features.metaTransactionsFeatureV2.getMetaTransactionV2Hash(mtxData), + INativeOrdersFeature.fillLimitOrder.selector, // 0xf6274f66 + USER_ADDRESS, + address(this) + ); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + + assertEq(zrx.balanceOf(signerAddress), 0); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(dai.balanceOf(signerAddress), 1e18); + assertEq(weth.balanceOf(address(this)), 1); + } + + function test_transformERC20WithAnySender() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + mtxData.sender = address(0); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(weth.balanceOf(address(this)), 1); + } + + function test_transformERC20WithoutFee() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionFeeData[] memory fees; + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransactionWithFees( + transformCallData, + fees + ); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(weth.balanceOf(address(this)), 0); // no fee paid out + } + + function test_transformERC20MultipleFees() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionFeeData[] + memory fees = new IMetaTransactionsFeatureV2.MetaTransactionFeeData[](2); + fees[0] = IMetaTransactionsFeatureV2.MetaTransactionFeeData({recipient: address(this), amount: 10}); + fees[1] = IMetaTransactionsFeatureV2.MetaTransactionFeeData({recipient: signerAddress, amount: 20}); + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransactionWithFees( + transformCallData, + fees + ); + + assertEq(dai.balanceOf(USER_ADDRESS), 1e18); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2( + mtxData, + _mtxSignature(mtxData) + ); + assertEq(zrx.balanceOf(USER_ADDRESS), 1e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(weth.balanceOf(address(this)), 10); + assertEq(weth.balanceOf(address(signerAddress)), 20); + } + + function test_transformERC20TranslatedCallFail() external { + bytes memory transformCallData = _badTokenTransformERC20Call(); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + LibSignature.Signature memory sig = _mtxSignature(mtxData); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_transformERC20UnsupportedFunction() external { + bytes memory transformCallData = _badSelectorTransformERC20Call(); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + LibSignature.Signature memory sig = _mtxSignature(mtxData); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_transformERC20CantExecuteTwice() external { + bytes memory callData = _makeTestRfqOrder(zrx, dai, signerAddress, USER_ADDRESS, signerKey); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(callData); + LibSignature.Signature memory sig = _mtxSignature(mtxData); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_metaTxnFailsIfExpired() external { + bytes memory callData = _makeTestRfqOrder(zrx, dai, signerAddress, USER_ADDRESS, signerKey); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(callData); + mtxData.expirationTimeSeconds = block.timestamp - 1; + + LibSignature.Signature memory sig = _mtxSignature(mtxData); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_metaTxnFailsIfWrongSender() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + mtxData.sender = USER_ADDRESS; + + LibSignature.Signature memory sig = _mtxSignature(mtxData); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_metaTxnFailsWrongSignature() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtxData = _getMetaTransaction(transformCallData); + + LibSignature.Signature memory sig = _mtxSignatureWithSignerKey(mtxData, signerKey); + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).executeMetaTransactionV2(mtxData, sig); + } + + function test_batchExecuteMetaTransactionsMultipleTransactions() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + bytes memory rfqCallData = _makeTestRfqOrder(zrx, shib, signerAddress, USER_ADDRESS, signerKey); + IMetaTransactionsFeatureV2.MetaTransactionDataV2[] + memory mtxns = new IMetaTransactionsFeatureV2.MetaTransactionDataV2[](2); + LibSignature.Signature[] memory sigs = new LibSignature.Signature[](2); + + mtxns[0] = _getMetaTransaction(transformCallData); + sigs[0] = _mtxSignature(mtxns[0]); + mtxns[1] = _getMetaTransaction(rfqCallData); + sigs[1] = _mtxSignature(mtxns[1]); + + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).batchExecuteMetaTransactionsV2(mtxns, sigs); + assertEq(zrx.balanceOf(USER_ADDRESS), 2e18); + assertEq(dai.balanceOf(USER_ADDRESS), 0); + assertEq(shib.balanceOf(USER_ADDRESS), 0); + } + + function test_batchExecuteMetaTransactionsCantExecuteSameTxnTwice() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + IMetaTransactionsFeatureV2.MetaTransactionDataV2[] + memory mtxns = new IMetaTransactionsFeatureV2.MetaTransactionDataV2[](2); + LibSignature.Signature[] memory sigs = new LibSignature.Signature[](2); + + mtxns[0] = _getMetaTransaction(transformCallData); + sigs[0] = _mtxSignature(mtxns[0]); + mtxns[1] = mtxns[0]; + sigs[1] = _mtxSignature(mtxns[1]); + + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).batchExecuteMetaTransactionsV2(mtxns, sigs); + } + + function test_batchExecuteMetaTransactionsFailsIfTransactionFails() external { + bytes memory transformCallData = _transformERC20Call(zrx, dai, USER_ADDRESS); + bytes memory badTransformCallData = _badTokenTransformERC20Call(); + IMetaTransactionsFeatureV2.MetaTransactionDataV2[] + memory mtxns = new IMetaTransactionsFeatureV2.MetaTransactionDataV2[](2); + LibSignature.Signature[] memory sigs = new LibSignature.Signature[](2); + + mtxns[0] = _getMetaTransaction(transformCallData); + sigs[0] = _mtxSignature(mtxns[0]); + mtxns[1] = _getMetaTransaction(badTransformCallData); + sigs[1] = _mtxSignature(mtxns[1]); + + vm.expectRevert(); + IMetaTransactionsFeatureV2(address(zeroExDeployed.zeroEx)).batchExecuteMetaTransactionsV2(mtxns, sigs); + } +} diff --git a/contracts/zero-ex/tests/MultiplexMetaTransactionsV2.t.sol b/contracts/zero-ex/tests/MultiplexMetaTransactionsV2.t.sol new file mode 100644 index 0000000000..d53178249d --- /dev/null +++ b/contracts/zero-ex/tests/MultiplexMetaTransactionsV2.t.sol @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; + +import {LocalTest} from "utils/LocalTest.sol"; +import {MultiplexUtils} from "utils/MultiplexUtils.sol"; +import {LibSignature} from "src/features/libs/LibSignature.sol"; +import {LibNativeOrder} from "src/features/libs/LibNativeOrder.sol"; +import {IMetaTransactionsFeatureV2} from "src/features/interfaces/IMetaTransactionsFeatureV2.sol"; + +contract MultiplexMetaTransactionsV2 is LocalTest, MultiplexUtils { + function _makeMetaTransactionV2( + bytes memory callData + ) private view returns (IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory, LibSignature.Signature memory) { + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx = IMetaTransactionsFeatureV2.MetaTransactionDataV2({ + signer: payable(otherSignerAddress), + sender: address(0), + expirationTimeSeconds: block.timestamp + 600, + salt: 123, + callData: callData, + feeToken: dai, + fees: new IMetaTransactionsFeatureV2.MetaTransactionFeeData[](0) + }); + + bytes32 mtxHash = zeroExDeployed.zeroEx.getMetaTransactionV2Hash(mtx); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(otherSignerKey, mtxHash); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + return (mtx, sig); + } + + function _executeMetaTransaction(bytes memory callData) private { + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx; + LibSignature.Signature memory sig; + (mtx, sig) = _makeMetaTransactionV2(callData); + zeroExDeployed.zeroEx.executeMetaTransactionV2(mtx, sig); + } + + // batch + + function test_metaTransaction_multiplexBatchSellTokenForToken_rfqOrder() external { + LibNativeOrder.RfqOrder memory rfqOrder = _makeTestRfqOrder(); + rfqOrder.taker = otherSignerAddress; + _mintTo(address(rfqOrder.takerToken), otherSignerAddress, rfqOrder.takerAmount); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeRfqSubcall(rfqOrder)), + rfqOrder.takerAmount, + rfqOrder.makerAmount + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_otcOrder() external { + LibNativeOrder.OtcOrder memory otcOrder = _makeTestOtcOrder(); + otcOrder.taker = otherSignerAddress; + _mintTo(address(otcOrder.takerToken), otherSignerAddress, otcOrder.takerAmount); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeOtcSubcall(otcOrder)), + otcOrder.takerAmount, + otcOrder.makerAmount + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_uniswapV2() external { + _createUniswapV2Pool(uniV2Factory, dai, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeUniswapV2BatchSubcall(_makeArray(address(dai), address(zrx)), 1e18, false)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_uniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeUniswapV3BatchSubcall(_makeArray(address(dai), address(zrx)), 1e18)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_liquidityProvider() external { + _mintTo(address(dai), otherSignerAddress, 1e18); + _mintTo(address(zrx), address(liquidityProvider), 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeMockLiquidityProviderBatchSubcall(1e18)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_transformErc20() external { + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeMockTransformERC20Subcall(dai, zrx, 1e18, 1e18)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_rfqOrderUniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + LibNativeOrder.RfqOrder memory rfqOrder = _makeTestRfqOrder(); + rfqOrder.taker = otherSignerAddress; + _mintTo(address(dai), otherSignerAddress, 2e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray( + _makeRfqSubcall(rfqOrder), + _makeUniswapV3BatchSubcall(_makeArray(address(dai), address(zrx)), 1e18) + ), + 2e18, + 11e18 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_rfqOrderFallbackUniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 2e18); + LibNativeOrder.RfqOrder memory rfqOrder = _makeTestRfqOrder(); + rfqOrder.taker = otherSignerAddress; + rfqOrder.expiry = 1; + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray( + _makeRfqSubcall(rfqOrder), + _makeUniswapV3BatchSubcall(_makeArray(address(dai), address(zrx)), 1e18) + ), + 1e18, + 10e18 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForToken_uniswapV3_revertsIfIncorrectAmount() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx; + LibSignature.Signature memory sig; + (mtx, sig) = _makeMetaTransactionV2( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray(_makeUniswapV3BatchSubcall(_makeArray(address(dai), address(zrx)), 5e17)), + 1e18, + 1 + ) + ); + + vm.expectRevert(); + zeroExDeployed.zeroEx.executeMetaTransactionV2(mtx, sig); + } + + // multi hop + + function test_metaTransaction_multiplexMultiHopSellTokenForToken_uniswapV2() external { + _createUniswapV2Pool(uniV2Factory, dai, zrx, 10e18, 10e18); + address[] memory tokens = _makeArray(address(dai), address(zrx)); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + tokens, + _makeArray(_makeUniswapV2MultiHopSubcall(tokens, false)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForToken_uniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + address[] memory tokens = _makeArray(address(dai), address(zrx)); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + tokens, + _makeArray(_makeUniswapV3MultiHopSubcall(tokens)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForToken_liquidityProvider() external { + _mintTo(address(dai), otherSignerAddress, 1e18); + _mintTo(address(zrx), address(liquidityProvider), 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + _makeArray(address(dai), address(zrx)), + _makeArray(_makeMockLiquidityProviderMultiHopSubcall()), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForToken_uniswapV2UniswapV3() external { + _createUniswapV2Pool(uniV2Factory, dai, shib, 10e18, 10e18); + _createUniswapV3Pool(uniV3Factory, shib, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + _makeArray(address(dai), address(shib), address(zrx)), + _makeArray( + _makeUniswapV2MultiHopSubcall(_makeArray(address(dai), address(shib)), false), + _makeUniswapV3MultiHopSubcall(_makeArray(address(shib), address(zrx))) + ), + 1e18, + 10e18 + ) + ); + } + + // batch for eth + + function test_metaTransaction_multiplexBatchSellTokenForEth_uniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, weth, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForEth.selector, + dai, + _makeArray(_makeUniswapV3BatchSubcall(_makeArray(address(dai), address(weth)), 1e18)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexBatchSellTokenForEth_rfqOrderUniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, weth, 10e18, 10e18); + LibNativeOrder.RfqOrder memory rfqOrder = _makeTestRfqOrder(); + rfqOrder.taker = otherSignerAddress; + rfqOrder.makerToken = weth; + _mintTo(address(weth), rfqOrder.maker, rfqOrder.makerAmount); + _mintTo(address(dai), otherSignerAddress, 2e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForEth.selector, + dai, + _makeArray( + _makeRfqSubcall(rfqOrder), + _makeUniswapV3BatchSubcall(_makeArray(address(dai), address(weth)), 1e18) + ), + 2e18, + 11e18 + ) + ); + } + + // nested + + function test_metaTransaction_multiplexBatchSellTokenForToken_nestedUniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + address[] memory tokens = _makeArray(address(dai), address(zrx)); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexBatchSellTokenForToken.selector, + dai, + zrx, + _makeArray( + _makeNestedMultiHopSellSubcall(tokens, _makeArray(_makeUniswapV3MultiHopSubcall(tokens)), 1e18) + ), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForToken_nestedUniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + _makeArray(address(dai), address(zrx)), + _makeArray( + _makeNestedBatchSellSubcall( + _makeArray(_makeUniswapV3BatchSubcall(_makeArray(address(dai), address(zrx)), 1e18)) + ) + ), + 1e18, + 1 + ) + ); + } + + // multi hop for eth + + function test_metaTransaction_multiplexMultiHopSellTokenForEth_uniswapV3() external { + _createUniswapV3Pool(uniV3Factory, dai, weth, 10e18, 10e18); + address[] memory tokens = _makeArray(address(dai), address(weth)); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForEth.selector, + tokens, + _makeArray(_makeUniswapV3MultiHopSubcall(tokens)), + 1e18, + 1 + ) + ); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForEth_uniswapV3_revertsNotWeth() external { + _createUniswapV3Pool(uniV3Factory, dai, zrx, 10e18, 10e18); + address[] memory tokens = _makeArray(address(dai), address(zrx)); + _mintTo(address(dai), otherSignerAddress, 1e18); + + IMetaTransactionsFeatureV2.MetaTransactionDataV2 memory mtx; + LibSignature.Signature memory sig; + (mtx, sig) = _makeMetaTransactionV2( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForEth.selector, + tokens, + _makeArray(_makeUniswapV3MultiHopSubcall(tokens)), + 1e18, + 1 + ) + ); + + vm.expectRevert("MetaTransactionsFeature::multiplexMultiHopSellTokenForEth/NOT_WETH"); + zeroExDeployed.zeroEx.executeMetaTransactionV2(mtx, sig); + } + + function test_metaTransaction_multiplexMultiHopSellTokenForEth_uniswapV2UniswapV3() external { + _createUniswapV2Pool(uniV2Factory, dai, shib, 10e18, 10e18); + _createUniswapV3Pool(uniV3Factory, shib, weth, 10e18, 10e18); + _mintTo(address(dai), otherSignerAddress, 1e18); + + _executeMetaTransaction( + abi.encodeWithSelector( + zeroExDeployed.zeroEx.multiplexMultiHopSellTokenForToken.selector, + _makeArray(address(dai), address(shib), address(weth)), + _makeArray( + _makeUniswapV2MultiHopSubcall(_makeArray(address(dai), address(shib)), false), + _makeUniswapV3MultiHopSubcall(_makeArray(address(shib), address(weth))) + ), + 1e18, + 10e18 + ) + ); + } +} diff --git a/contracts/zero-ex/tests/forked/MultiplexRfqTest.t.sol b/contracts/zero-ex/tests/forked/MultiplexRfqTest.t.sol new file mode 100644 index 0000000000..fb53d566d8 --- /dev/null +++ b/contracts/zero-ex/tests/forked/MultiplexRfqTest.t.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + Copyright 2023 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; + +pragma experimental ABIEncoderV2; + +import "../utils/ForkUtils.sol"; +import "../utils/TestUtils.sol"; +import "src/IZeroEx.sol"; +import "@0x/contracts-erc20/src/IEtherToken.sol"; +import "src/features/TransformERC20Feature.sol"; +import "src/features/multiplex/MultiplexFeature.sol"; +import "src/external/TransformerDeployer.sol"; +import "src/transformers/WethTransformer.sol"; +import "src/transformers/FillQuoteTransformer.sol"; +import "src/transformers/bridges/BridgeProtocols.sol"; +import "src/features/OtcOrdersFeature.sol"; + +contract MultiplexRfqtTest is Test, ForkUtils, TestUtils { + using LibERC20TokenV06 for IERC20Token; + using LibERC20TokenV06 for IEtherToken; + + function setUp() public { + _setup(); + } + + function test_swapEthForUSDTThroughFqtOtcs() public { + log_string("SwapEthForUSDTThroughFqtOtc"); + /* */ + for (uint256 i = 0; i < 1; i++) { + //skip fantom/avax failing test + if (i == 3 || i == 4) { + continue; + } + vm.selectFork(forkIds[chains[i]]); + log_named_string(" Selecting Fork On", chains[i]); + vm.deal(address(this), 1e18); + labelAddresses( + chains[i], + indexChainsByChain[chains[i]], + getTokens(i), + getContractAddresses(i), + getLiquiditySourceAddresses(i) + ); + + //redeploy and migrate multiplexFeature and OtcOrders for logging + + MultiplexFeature multiplexFeature = new MultiplexFeature( + address(IZERO_EX), + IEtherToken(tokens.WrappedNativeToken), + ILiquidityProviderSandbox(addresses.exchangeProxyLiquidityProviderSandbox), + address(0), // uniswapFactory + address(0), // sushiswapFactory + bytes32(0), // uniswapPairInitCodeHash + bytes32(0) // sushiswapPairInitCodeHash + ); + + OtcOrdersFeature otcOrdersFeature = new OtcOrdersFeature( + address(addresses.exchangeProxy), + tokens.WrappedNativeToken + ); + + vm.label(address(multiplexFeature), "zeroEx/NewMultiplexFeature"); + vm.label(address(otcOrdersFeature), "zeroEx/NewOtcOrdersFeature"); + vm.prank(IZeroEx(addresses.exchangeProxy).owner()); + + IZeroEx(addresses.exchangeProxy).migrate( + address(otcOrdersFeature), + abi.encodeWithSelector(OtcOrdersFeature.migrate.selector), + address(addresses.exchangeProxy) + ); + vm.prank(IZeroEx(addresses.exchangeProxy).owner()); + IZeroEx(addresses.exchangeProxy).migrate( + address(multiplexFeature), + abi.encodeWithSelector(MultiplexFeature.migrate.selector), + address(addresses.exchangeProxy) + ); + swapMultihopOtc(getTokens(i), getContractAddresses(i), getLiquiditySourceAddresses(i)); + } + } + + /* solhint-disable function-max-lines */ + + function swapMultihopOtc( + TokenAddresses memory tokens, + ContractAddresses memory addresses, + LiquiditySources memory sources + ) public onlyForked { + IZERO_EX = IZeroEx(addresses.exchangeProxy); + + address[] memory tradeTokens = new address[](3); + tradeTokens[0] = address(tokens.WrappedNativeToken); + tradeTokens[1] = address(tokens.USDC); + tradeTokens[2] = address(tokens.DAI); + + deal(tradeTokens[0], address(this), 1e18); + + tokens.WrappedNativeToken.approveIfBelow(addresses.exchangeProxy, uint(-1)); + uint inputAmount = 1e18; + uint outputAmount = 5e17; + + IMultiplexFeature.MultiHopSellSubcall[] memory subcalls = new IMultiplexFeature.MultiHopSellSubcall[](2); + + IMultiplexFeature.MultiHopSellSubcall memory subcall1; + subcall1.id = IMultiplexFeature.MultiplexSubcall.OTC; + + //subcall.data = abi.encode(address[], LibNativeOrder.OtcOrder, LibSignature.Signature); + (LibNativeOrder.OtcOrder memory order1, LibSignature.Signature memory signature1) = createOtcOrder( + tokens.WrappedNativeToken, + tokens.USDC, + 1e18, + 5e17, + 0 + ); + subcall1.data = abi.encode(order1, signature1); + + IMultiplexFeature.MultiHopSellSubcall memory subcall2; + subcall2.id = IMultiplexFeature.MultiplexSubcall.OTC; + (LibNativeOrder.OtcOrder memory order2, LibSignature.Signature memory signature2) = createOtcOrder( + tokens.USDC, + tokens.DAI, + 5e17, + 5e17, + 1 + ); + subcall2.data = abi.encode(order2, signature2); + + subcalls[0] = subcall1; + subcalls[1] = subcall2; + + uint balanceBefore = tokens.DAI.balanceOf(address(this)); + emit log_named_uint("DAI Balance Before", balanceBefore); + emit log_string("Multihop Rfqt: WETH->USDC->DAI"); + + /// @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. + IZERO_EX.multiplexMultiHopSellTokenForToken( + // input token[] [input, intermediate, output] + tradeTokens, + //array of subcalls [{},{}] + subcalls, + // input token amount + inputAmount, + // min output token amount + outputAmount + ); + uint balanceAfter = tokens.DAI.balanceOf(address(this)); + emit log_named_uint("DAI Balance After", balanceAfter - balanceBefore); + require(balanceAfter >= 5e17, "Failed: UNDERBOUGHT"); + } + + function createOtcOrder( + IERC20Token inputToken, + IERC20Token ouputToken, + uint128 takerAmount, + uint128 makerAmount, + uint bump + ) public returns (LibNativeOrder.OtcOrder memory order, LibSignature.Signature memory signature) { + LibNativeOrder.OtcOrder memory order; + LibSignature.Signature memory signature; + order.makerToken = ouputToken; + order.takerToken = inputToken; + order.takerAmount = takerAmount; + order.makerAmount = makerAmount; + uint privateKey; + + (order.maker, privateKey) = _getSigner(); + + deal(address(order.makerToken), order.maker, 2e20); + deal(address(order.takerToken), order.maker, 2e20); + + vm.startPrank(order.maker); + IERC20Token(order.makerToken).approveIfBelow(addresses.exchangeProxy, 2e20); + + order.taker = address(0); + order.txOrigin = address(tx.origin); + order.expiryAndNonce = encodeExpiryAndNonce(order.maker, bump); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, IZERO_EX.getOtcOrderHash(order)); + + vm.stopPrank(); + signature.signatureType = LibSignature.SignatureType.EIP712; + signature.v = v; + signature.r = r; + signature.s = s; + + return (order, signature); + } + + /* solhint-enable function-max-lines */ + function encodeExpiryAndNonce(address maker, uint bump) public returns (uint256) { + uint256 expiry = (block.timestamp + 120) << 192; + uint256 bucket = (0 + bump) << 128; + uint256 nonce = vm.getNonce(maker); + return expiry | bucket | nonce; + } +} diff --git a/contracts/zero-ex/tests/utils/DeployZeroEx.sol b/contracts/zero-ex/tests/utils/DeployZeroEx.sol index 9d9f231a60..20000b8875 100644 --- a/contracts/zero-ex/tests/utils/DeployZeroEx.sol +++ b/contracts/zero-ex/tests/utils/DeployZeroEx.sol @@ -28,6 +28,7 @@ import "src/features/FundRecoveryFeature.sol"; import "src/features/TransformERC20Feature.sol"; import "src/features/OtcOrdersFeature.sol"; import "src/features/MetaTransactionsFeature.sol"; +import "src/features/MetaTransactionsFeatureV2.sol"; import "src/features/nft_orders/ERC1155OrdersFeature.sol"; import "src/features/nft_orders/ERC721OrdersFeature.sol"; import "src/features/UniswapFeature.sol"; @@ -68,6 +69,7 @@ contract DeployZeroEx is Test { FundRecoveryFeature fundRecoveryFeature; TransformERC20Feature transformERC20Feature; MetaTransactionsFeature metaTransactionsFeature; + MetaTransactionsFeatureV2 metaTransactionsFeatureV2; ERC1155OrdersFeature erc1155OrdersFeature; ERC721OrdersFeature erc721OrdersFeature; MultiplexFeature multiplexFeature; @@ -133,6 +135,10 @@ contract DeployZeroEx is Test { emit log_named_address("UniswapV3Feature", address(ZERO_EX_DEPLOYED.features.uniswapV3Feature)); emit log_named_address("FundRecoveryFeature", address(ZERO_EX_DEPLOYED.features.fundRecoveryFeature)); emit log_named_address("MetaTransactionsFeature", address(ZERO_EX_DEPLOYED.features.metaTransactionsFeature)); + emit log_named_address( + "MetaTransactionsFeatureV2", + address(ZERO_EX_DEPLOYED.features.metaTransactionsFeatureV2) + ); emit log_named_address("ERC1155OrdersFeature", address(ZERO_EX_DEPLOYED.features.erc1155OrdersFeature)); emit log_named_address("ERC721OrdersFeature", address(ZERO_EX_DEPLOYED.features.erc721OrdersFeature)); emit log_named_address("TransformERC20Feature", address(ZERO_EX_DEPLOYED.features.transformERC20Feature)); @@ -201,6 +207,10 @@ contract DeployZeroEx is Test { ); ZERO_EX_DEPLOYED.features.fundRecoveryFeature = new FundRecoveryFeature(); ZERO_EX_DEPLOYED.features.metaTransactionsFeature = new MetaTransactionsFeature(address(ZERO_EX)); + ZERO_EX_DEPLOYED.features.metaTransactionsFeatureV2 = new MetaTransactionsFeatureV2( + address(ZERO_EX), + ZERO_EX_DEPLOYED.weth + ); ZERO_EX_DEPLOYED.features.erc1155OrdersFeature = new ERC1155OrdersFeature( address(ZERO_EX), ZERO_EX_DEPLOYED.weth @@ -270,6 +280,11 @@ contract DeployZeroEx is Test { abi.encodeWithSelector(MetaTransactionsFeature.migrate.selector), address(this) ); + IZERO_EX.migrate( + address(ZERO_EX_DEPLOYED.features.metaTransactionsFeatureV2), + abi.encodeWithSelector(MetaTransactionsFeatureV2.migrate.selector), + address(this) + ); IZERO_EX.migrate( address(ZERO_EX_DEPLOYED.features.erc1155OrdersFeature), abi.encodeWithSelector(ERC1155OrdersFeature.migrate.selector), diff --git a/contracts/zero-ex/tests/utils/LocalTest.sol b/contracts/zero-ex/tests/utils/LocalTest.sol index d127d1b910..d3998a1b5a 100644 --- a/contracts/zero-ex/tests/utils/LocalTest.sol +++ b/contracts/zero-ex/tests/utils/LocalTest.sol @@ -54,6 +54,9 @@ contract LocalTest is Test, TestUtils { address internal signerAddress; uint256 internal signerKey; + address internal otherSignerAddress; + uint256 internal otherSignerKey; + function _infiniteApprovals() private { shib.approve(address(zeroExDeployed.zeroEx), type(uint256).max); dai.approve(address(zeroExDeployed.zeroEx), type(uint256).max); @@ -92,12 +95,23 @@ contract LocalTest is Test, TestUtils { zrx = IERC20Token(address(new TestMintableERC20Token())); weth = zeroExDeployed.weth; + // TODO this should be somewhere else + string memory mnemonic = "conduct into noodle wreck before satisfy alarm vendor dose lunch vapor party"; + otherSignerKey = vm.deriveKey(mnemonic, 0); + otherSignerAddress = vm.addr(otherSignerKey); + vm.label(otherSignerAddress, "zeroEx/OtherGuy"); + _infiniteApprovals(); + vm.startPrank(signerAddress); _infiniteApprovals(); vm.stopPrank(); - vm.deal(address(this), 10e18); + vm.startPrank(otherSignerAddress); + _infiniteApprovals(); + vm.stopPrank(); + + vm.deal(address(this), 20e18); } function _mintTo(address token, address recipient, uint256 amount) internal { diff --git a/contracts/zero-ex/tests/utils/TestUtils.sol b/contracts/zero-ex/tests/utils/TestUtils.sol index d9375fbf3e..f69cfb7920 100644 --- a/contracts/zero-ex/tests/utils/TestUtils.sol +++ b/contracts/zero-ex/tests/utils/TestUtils.sol @@ -20,6 +20,8 @@ import "forge-std/Test.sol"; import "src/transformers/LibERC20Transformer.sol"; contract TestUtils is Test { + address private constant ZERO_ADDRESS = 0x0000000000000000000000000000000000000000; + function _findTransformerNonce(address transformer, address deployer) internal pure returns (uint32) { address current; for (uint32 i = 0; i < 1024; i++) { diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 49ed2eb0a2..55d3158d27 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -30,6 +30,7 @@ "generated-artifacts/LiquidityProviderFeature.json", "generated-artifacts/LogMetadataTransformer.json", "generated-artifacts/MetaTransactionsFeature.json", + "generated-artifacts/MetaTransactionsFeatureV2.json", "generated-artifacts/MultiplexFeature.json", "generated-artifacts/NativeOrdersFeature.json", "generated-artifacts/OptimismBridgeAdapter.json", @@ -88,6 +89,7 @@ "test/generated-artifacts/ILiquidityProviderFeature.json", "test/generated-artifacts/ILiquidityProviderSandbox.json", "test/generated-artifacts/IMetaTransactionsFeature.json", + "test/generated-artifacts/IMetaTransactionsFeatureV2.json", "test/generated-artifacts/IMooniswapPool.json", "test/generated-artifacts/IMultiplexFeature.json", "test/generated-artifacts/INativeOrdersEvents.json", @@ -141,6 +143,7 @@ "test/generated-artifacts/LiquidityProviderSandbox.json", "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", + "test/generated-artifacts/MetaTransactionsFeatureV2.json", "test/generated-artifacts/MixinAaveV2.json", "test/generated-artifacts/MixinBalancer.json", "test/generated-artifacts/MixinBalancerV2Batch.json",