diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index d000261fb7..2c810e82f6 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -31,6 +31,8 @@ import "./interfaces/INativeOrdersFeature.sol"; import "./interfaces/ITransformERC20Feature.sol"; import "./libs/LibSignature.sol"; +import "forge-std/console.sol"; + /// @dev MetaTransactions feature. contract MetaTransactionsFeature is IFeature, @@ -253,24 +255,28 @@ contract MetaTransactionsFeature is /// @dev Validate that a meta-transaction is executable. function _validateMetaTransaction(ExecuteState memory state) private view { + console.log("Must be from required sender, if set"); // 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(); } + console.log("Must not be expired"); // Must not be expired. if (state.mtx.expirationTimeSeconds <= block.timestamp) { LibMetaTransactionsRichErrors .MetaTransactionExpiredError(state.hash, block.timestamp, state.mtx.expirationTimeSeconds) .rrevert(); } + console.log("Must have a valid gas price"); // Must have a valid gas price. if (state.mtx.minGasPrice > tx.gasprice || state.mtx.maxGasPrice < tx.gasprice) { LibMetaTransactionsRichErrors .MetaTransactionGasPriceError(state.hash, tx.gasprice, state.mtx.minGasPrice, state.mtx.maxGasPrice) .rrevert(); } + console.log("Must have enough ETH"); // Must have enough ETH. state.selfBalance = address(this).balance; if (state.mtx.value > state.selfBalance) { @@ -291,6 +297,7 @@ contract MetaTransactionsFeature is ) .rrevert(); } + console.log("Must not have already been executed"); // Transaction must not have been already executed. state.executedBlockNumber = LibMetaTransactionsStorage.getStorage().mtxHashToExecutedBlockNumber[state.hash]; if (state.executedBlockNumber != 0) { diff --git a/contracts/zero-ex/tests/MetaTransactionTest.t.sol b/contracts/zero-ex/tests/MetaTransactionTest.t.sol new file mode 100644 index 0000000000..ba4c32721d --- /dev/null +++ b/contracts/zero-ex/tests/MetaTransactionTest.t.sol @@ -0,0 +1,321 @@ +// 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/DeployZeroEx.sol"; +import "../contracts/src/features/MetaTransactionsFeature.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-erc20/contracts/src/v06/IEtherTokenV06.sol"; + +contract MetaTransactionTest is BaseTest { + DeployZeroEx.ZeroExDeployed zeroExDeployed; + address private constant ZERO_ADDRESS = 0x0000000000000000000000000000000000000000; + address private constant USER_ADDRESS = 0x6dc3a54FeAE57B65d185A7B159c5d3FA7fD7FD0F; + uint256 private constant USER_KEY = 0x1fc1630343b31e60b7a197a53149ca571ed9d9791e2833337bbd8110c30710ec; + IEtherTokenV06 private wethToken; + IERC20TokenV06 private usdcToken; + IERC20TokenV06 private zrxToken; + uint256 private constant oneEth = 1e18; + address private signerAddress; + uint256 private signerKey; + uint256 private transformerNonce; + + + function setUp() public { + (signerAddress, signerKey) = getSigner(); + zeroExDeployed = new DeployZeroEx().deployZeroEx(); + wethToken = zeroExDeployed.weth; + usdcToken = IERC20TokenV06(address(new TestMintableERC20Token())); + zrxToken = IERC20TokenV06(address(new TestMintableERC20Token())); + + transformerNonce = zeroExDeployed.transformerDeployer.nonce(); + vm.prank(zeroExDeployed.transformerDeployer.authorities(0)); + zeroExDeployed.transformerDeployer.deploy(type(TestMintTokenERC20Transformer).creationCode); + + vm.deal(address(this), 10e18); + vm.deal(USER_ADDRESS, 10e18); + vm.deal(signerAddress, 10e18); + } + + function getSigner() public returns (address, uint) { + string memory mnemonic = "test test test test test test test test test test test junk"; + uint256 privateKey = vm.deriveKey(mnemonic, 0); + vm.label(vm.addr(privateKey), "zeroEx/MarketMaker"); + return (vm.addr(privateKey), privateKey); + } + + function makeTestRfqOrder() private returns (bytes memory callData) { + LibNativeOrder.RfqOrder memory order = LibNativeOrder.RfqOrder({ + makerToken: wethToken, + takerToken: usdcToken, + makerAmount: 1e18, + takerAmount: 1e18, + maker: signerAddress, + taker: ZERO_ADDRESS, + 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), USER_ADDRESS, order.takerAmount); + vm.prank(USER_ADDRESS); + order.takerToken.approve(address(zeroExDeployed.zeroEx), order.takerAmount); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, zeroExDeployed.features.nativeOrdersFeature.getRfqOrderHash(order)); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + + return abi.encodeWithSelector( + INativeOrdersFeature.fillRfqOrder.selector, // ?? + order, // RFQOrder + sig, // Order Signature + 1e18 // Fill Amount + ); + } + + function mintTo(address token, address recipient, uint256 amount) private { + if (token == address(wethToken)) { + //vm.prank(recipient); + IEtherTokenV06(token).deposit{value: amount}(); + WETH9V06(payable(token)).transfer(recipient, amount); + } else { + TestMintableERC20Token(token).mint(recipient, amount); + } + } + + function getRandomMetaTransaction(bytes memory callData) private view returns (IMetaTransactionsFeature.MetaTransactionData memory) { + IMetaTransactionsFeature.MetaTransactionData memory mtx = IMetaTransactionsFeature.MetaTransactionData({ + signer: payable(USER_ADDRESS), + sender: address(this), + minGasPrice: 0, + maxGasPrice: 100000000000, + expirationTimeSeconds: block.timestamp + 60, + salt: 123, + callData: callData, + value: 0, + feeToken: wethToken, + feeAmount: 1 + }); + return mtx; + } + + function transformERC20Call() private returns (bytes memory) { + ITransformERC20Feature.Transformation[] memory transformations = new ITransformERC20Feature.Transformation[](1); + transformations[0] = ITransformERC20Feature.Transformation( + uint32(transformerNonce), + abi.encode(address(usdcToken), address(zrxToken), 0, oneEth, 0) + ); + + mintTo(address(usdcToken), USER_ADDRESS, oneEth); + vm.prank(USER_ADDRESS); + usdcToken.approve(address(zeroExDeployed.zeroEx), oneEth); + + return abi.encodeWithSelector( + zeroExDeployed.zeroEx.transformERC20.selector, // 0x415565b0 + usdcToken, + zrxToken, + oneEth, + oneEth, + transformations + ); + } + + function mtxCall(IMetaTransactionsFeature.MetaTransactionData memory mtx) private returns (bytes memory) { + // Mint fee to signer and approve + if (mtx.feeAmount > 0) { + mintTo(address(mtx.feeToken), mtx.signer, mtx.feeAmount); + vm.prank(mtx.signer); + mtx.feeToken.approve(address(zeroExDeployed.zeroEx), oneEth); + } + + bytes32 mtxHash = zeroExDeployed.features.metaTransactionsFeature.getMetaTransactionHash(mtx); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(USER_KEY, mtxHash); + LibSignature.Signature memory sig = LibSignature.Signature(LibSignature.SignatureType.EIP712, v, r, s); + + return abi.encodeWithSelector( + zeroExDeployed.zeroEx.executeMetaTransaction.selector, // 0x3d61ed3e + mtx, + sig + ); + } + + function test_createHash() public { + bytes memory transformCallData = transformERC20Call(); + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(transformCallData); + + //mtxData.signer = address(0); + bytes32 mtxHash = zeroExDeployed.features.metaTransactionsFeature.getMetaTransactionHash(mtxData); + assertTrue(mtxHash != bytes32(0)); + } + + function test_transformERC20() public { + bytes memory transformCallData = transformERC20Call(); + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(transformCallData); + + bytes memory theCallData = mtxCall(mtxData); + + assertEq(usdcToken.balanceOf(USER_ADDRESS), oneEth); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertTrue(success); + assertEq(zrxToken.balanceOf(USER_ADDRESS), oneEth); + assertEq(usdcToken.balanceOf(USER_ADDRESS), 0); + assertEq(wethToken.balanceOf(address(this)), 1); + } + + function test_rfqOrder() public { + bytes memory callData = makeTestRfqOrder(); + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(callData); + + bytes memory rfqCallData = mtxCall(mtxData); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0 }(rfqCallData); + + assertTrue(success); + assertEq(wethToken.balanceOf(signerAddress), 0); + assertEq(wethToken.balanceOf(USER_ADDRESS), 1e18); + assertEq(usdcToken.balanceOf(USER_ADDRESS), 0); + assertEq(usdcToken.balanceOf(signerAddress), 1e18); + assertEq(wethToken.balanceOf(address(this)), 1); + + // TODO: check event log for TestMetaTransactionsNativeOrdersFeatureEvents.FillLimitOrderCalled? + } + + function test_fillLimitOrder() public { + + } + + function test_transformERC20WithAnySender() public { + bytes memory transformCallData = transformERC20Call(); + + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(transformCallData); + mtxData.sender = ZERO_ADDRESS; + + bytes memory theCallData = mtxCall(mtxData); + + assertEq(usdcToken.balanceOf(USER_ADDRESS), oneEth); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertTrue(success); + assertEq(zrxToken.balanceOf(USER_ADDRESS), oneEth); + assertEq(usdcToken.balanceOf(USER_ADDRESS), 0); + assertEq(wethToken.balanceOf(address(this)), 1); + } + + function test_transformERC20WithoutFee() public { + bytes memory transformCallData = transformERC20Call(); + + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(transformCallData); + mtxData.feeAmount = 0; + + bytes memory theCallData = mtxCall(mtxData); + + assertEq(usdcToken.balanceOf(USER_ADDRESS), oneEth); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertTrue(success); + assertEq(zrxToken.balanceOf(USER_ADDRESS), oneEth); + assertEq(usdcToken.balanceOf(USER_ADDRESS), 0); + assertEq(wethToken.balanceOf(address(this)), 0); // no fee paid out + } + + function test_transformERC20TranslatedCallFail() public { + + } + + function test_transformERC20UnsupportedFunction() public { + + } + + function test_transformERC20CantExecuteTwice() public { + + } + + function test_metaTxnFailsNotEnoughEth() public { + bytes memory callData = makeTestRfqOrder(); + + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(callData); + mtxData.value = 1; + + bytes memory theCallData = mtxCall(mtxData); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertFalse(success); + } +/* + function test_metaTxnFailsGasPriceTooLow() public { + + } + + function test_metaTxnFailsGasPriceTooHigh() public { + + }*/ + + function test_metaTxnFailsIfExpired() public { + bytes memory callData = makeTestRfqOrder(); + + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(callData); + mtxData.expirationTimeSeconds = block.timestamp; + + bytes memory theCallData = mtxCall(mtxData); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertFalse(success); + } + + function test_metaTxnFailsIfWrongSender() public { + bytes memory transformCallData = transformERC20Call(); + IMetaTransactionsFeature.MetaTransactionData memory mtxData = getRandomMetaTransaction(transformCallData); + mtxData.sender = USER_ADDRESS; + + bytes memory theCallData = mtxCall(mtxData); + + assertEq(usdcToken.balanceOf(USER_ADDRESS), oneEth); + + (bool success, ) = address(zeroExDeployed.zeroEx).call{ value: 0}(theCallData); + assertFalse(success); + } + + function test_metaTxnFailsWrongSignature() public { + + } + + function test_metaTxnCantReenterExecuteMetaTransaction() public { + + } + + function test_metaTxnCantReenterBatchExecuteMetaTransaction() public { + + } + + function test_metaTxnCantReduceInitialEthBalance() public { + + } +} \ No newline at end of file