From 5f5a158060ad6a449a84f03ccb3909f7c2e543c4 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 8 Jul 2020 14:43:27 -0400 Subject: [PATCH] `@0x/contracts-zero-ex`: Update `TransformERC20` and `MetaTransactions` to handle signed calldata. --- contracts/zero-ex/CHANGELOG.json | 4 + .../src/features/IMetaTransactions.sol | 2 +- .../src/features/ITransformERC20.sol | 69 +- .../src/features/MetaTransactions.sol | 43 +- .../src/features/SignatureValidator.sol | 17 +- .../contracts/src/features/TransformERC20.sol | 196 ++-- .../src/features/libs/LibSignedCallData.sol | 67 ++ .../src/storage/LibTransformERC20Storage.sol | 2 + ...tMetaTransactionsTransformERC20Feature.sol | 30 +- contracts/zero-ex/src/index.ts | 1 + contracts/zero-ex/src/signed_call_data.ts | 29 + contracts/zero-ex/test/artifacts.ts | 2 + .../test/features/meta_transactions_test.ts | 66 +- .../test/features/transform_erc20_test.ts | 840 ++++++++++++------ contracts/zero-ex/test/full_migration_test.ts | 2 + contracts/zero-ex/test/wrappers.ts | 1 + contracts/zero-ex/tsconfig.json | 1 + packages/abi-gen/CHANGELOG.json | 9 + .../partials/method_call.handlebars | 4 +- .../TypeScript/partials/method_tx.handlebars | 4 +- .../output/typescript/abi_gen_dummy.ts | 80 +- .../output/typescript/test_lib_dummy.ts | 4 +- 22 files changed, 979 insertions(+), 494 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/features/libs/LibSignedCallData.sol create mode 100644 contracts/zero-ex/src/signed_call_data.ts diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 50f9e195db..69e75db6b6 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Add `MetaTransactions` and `SignatureValidator` features", "pr": 2610 + }, + { + "note": "Update `TransformERC20` and `MetaTransactions` to handle signed calldata.", + "pr": 2626 } ], "timestamp": 1594788383 diff --git a/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol index 16be3986b8..0c9f4e4b9a 100644 --- a/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol +++ b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol @@ -28,7 +28,7 @@ interface IMetaTransactions { /// @dev Describes an exchange proxy meta transaction. struct MetaTransactionData { // Signer of meta-transaction. On whose behalf to execute the MTX. - address signer; + address payable signer; // Required sender, or NULL for anyone. address sender; // Minimum gas price. diff --git a/contracts/zero-ex/contracts/src/features/ITransformERC20.sol b/contracts/zero-ex/contracts/src/features/ITransformERC20.sol index 416c47eb62..b9a8894219 100644 --- a/contracts/zero-ex/contracts/src/features/ITransformERC20.sol +++ b/contracts/zero-ex/contracts/src/features/ITransformERC20.sol @@ -37,6 +37,33 @@ interface ITransformERC20 { bytes data; } + /// @dev Arguments for `_transformERC20()`. + struct TransformERC20Args { + // The taker address. + address payable taker; + // The token being provided by the taker. + // If `0xeee...`, ETH is implied and should be provided with the call.` + IERC20TokenV06 inputToken; + // The token to be acquired by the taker. + // `0xeee...` implies ETH. + IERC20TokenV06 outputToken; + // The amount of `inputToken` to take from the taker. + // If set to `uint256(-1)`, the entire spendable balance of the taker + // will be solt. + uint256 inputTokenAmount; + // The minimum amount of `outputToken` the taker + // must receive for the entire transformation to succeed. If set to zero, + // the minimum output token transfer will not be asserted. + uint256 minOutputTokenAmount; + // The transformations to execute on the token balance(s) + // in sequence. + Transformation[] transformations; + // The hash of the calldata for the `transformERC20()` call. + bytes32 callDataHash; + // The signature for `callDataHash` signed by `getQuoteSigner()`. + bytes callDataSignature; + } + /// @dev Raised upon a successful `transformERC20`. /// @param taker The taker (caller) address. /// @param inputToken The token being provided by the taker. @@ -57,12 +84,23 @@ interface ITransformERC20 { /// @param transformerDeployer The new deployer address. event TransformerDeployerUpdated(address transformerDeployer); + /// @dev Raised when `setQuoteSigner()` is called. + /// @param quoteSigner The new quote signer. + event QuoteSignerUpdated(address quoteSigner); + /// @dev Replace the allowed deployer for transformers. /// Only callable by the owner. - /// @param transformerDeployer The address of the trusted deployer for transformers. + /// @param transformerDeployer The address of the new trusted deployer + /// for transformers. function setTransformerDeployer(address transformerDeployer) external; + /// @dev Replace the optional signer for `transformERC20()` calldata. + /// Only callable by the owner. + /// @param quoteSigner The address of the new calldata signer. + function setQuoteSigner(address quoteSigner) + external; + /// @dev Deploy a new flash wallet instance and replace the current one with it. /// Useful if we somehow break the current wallet instance. /// Only callable by the owner. @@ -95,27 +133,9 @@ interface ITransformERC20 { returns (uint256 outputTokenAmount); /// @dev Internal version of `transformERC20()`. Only callable from within. - /// @param callDataHash Hash of the ingress calldata. - /// @param taker The taker address. - /// @param inputToken The token being provided by the taker. - /// If `0xeee...`, ETH is implied and should be provided with the call.` - /// @param outputToken The token to be acquired by the taker. - /// `0xeee...` implies ETH. - /// @param inputTokenAmount The amount of `inputToken` to take from the taker. - /// @param minOutputTokenAmount The minimum amount of `outputToken` the taker - /// must receive for the entire transformation to succeed. - /// @param transformations The transformations to execute on the token balance(s) - /// in sequence. + /// @param args A `TransformERC20Args` struct. /// @return outputTokenAmount The amount of `outputToken` received by the taker. - function _transformERC20( - bytes32 callDataHash, - address payable taker, - IERC20TokenV06 inputToken, - IERC20TokenV06 outputToken, - uint256 inputTokenAmount, - uint256 minOutputTokenAmount, - Transformation[] calldata transformations - ) + function _transformERC20(TransformERC20Args calldata args) external payable returns (uint256 outputTokenAmount); @@ -134,4 +154,11 @@ interface ITransformERC20 { external view returns (address deployer); + + /// @dev Return the optional signer for `transformERC20()` calldata. + /// @return signer The transform deployer address. + function getQuoteSigner() + external + view + returns (address signer); } diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactions.sol b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol index 55b73ed92d..975520a6a5 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactions.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol @@ -26,6 +26,7 @@ import "../fixins/FixinCommon.sol"; import "../fixins/FixinEIP712.sol"; import "../migrations/LibMigrate.sol"; import "../storage/LibMetaTransactionsStorage.sol"; +import "./libs/LibSignedCallData.sol"; import "./IMetaTransactions.sol"; import "./ITransformERC20.sol"; import "./ISignatureValidator.sol"; @@ -43,18 +44,27 @@ contract MetaTransactions is using LibBytesV06 for bytes; using LibRichErrorsV06 for bytes; - /// @dev Intermediate state vars to avoid stack overflows. + /// @dev Intermediate state vars used by `_executeMetaTransactionPrivate()` + /// to avoid stack overflows. struct ExecuteState { + // Sender of the meta-transaction. address sender; + // Hash of the meta-transaction data. bytes32 hash; + // The meta-transaction data. MetaTransactionData mtx; + // The meta-transaction signature (by `mtx.signer`). bytes 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; } - struct TransformERC20Args { + /// @dev Arguments for a `TransformERC20.transformERC20()` call. + struct ExternalTransformERC20Args { IERC20TokenV06 inputToken; IERC20TokenV06 outputToken; uint256 inputTokenAmount; @@ -379,7 +389,7 @@ contract MetaTransactions is // | transformations (offset) | 160 | = 32 // | transformations (data) | 192 | - TransformERC20Args memory args; + 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. @@ -388,8 +398,8 @@ contract MetaTransactions is uint256 fromMem; uint256 toMem; assembly { - // Prefix the original calldata with a struct offset, - // which is just one word over. + // 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) @@ -398,20 +408,27 @@ contract MetaTransactions is } LibBytesV06.memCopy(toMem, fromMem, fromCallData.length - 4); // Decode call args for `ITransformERC20.transformERC20()` as a struct. - args = abi.decode(encodedStructArgs, (TransformERC20Args)); + args = abi.decode(encodedStructArgs, (ExternalTransformERC20Args)); } + // Parse the signature and hash out of the calldata so `_transformERC20()` + // can authenticate it. + (bytes32 callDataHash, bytes memory callDataSignature) = + LibSignedCallData.parseCallData(state.mtx.callData); // Call `ITransformERC20._transformERC20()` (internal variant). return _callSelf( state.hash, abi.encodeWithSelector( ITransformERC20._transformERC20.selector, - keccak256(state.mtx.callData), - state.mtx.signer, // taker is mtx signer - args.inputToken, - args.outputToken, - args.inputTokenAmount, - args.minOutputTokenAmount, - args.transformations + ITransformERC20.TransformERC20Args({ + taker: state.mtx.signer, // taker is mtx signer + inputToken: args.inputToken, + outputToken: args.outputToken, + inputTokenAmount: args.inputTokenAmount, + minOutputTokenAmount: args.minOutputTokenAmount, + transformations: args.transformations, + callDataHash: callDataHash, + callDataSignature: callDataSignature + }) ), state.mtx.value ); diff --git a/contracts/zero-ex/contracts/src/features/SignatureValidator.sol b/contracts/zero-ex/contracts/src/features/SignatureValidator.sol index 27a8251868..b7e6b1b71a 100644 --- a/contracts/zero-ex/contracts/src/features/SignatureValidator.sol +++ b/contracts/zero-ex/contracts/src/features/SignatureValidator.sol @@ -106,11 +106,18 @@ contract SignatureValidator is override returns (bool isValid) { - try this.validateHashSignature(hash, signer, signature) { - isValid = true; - } catch (bytes memory) { - isValid = false; - } + // HACK: `validateHashSignature()` is stateless so we can just perform + // a staticcall against the implementation contract. This avoids the + // overhead of going through the proxy. If `validateHashSignature()` ever + // becomes stateful this would need to change. + (isValid, ) = _implementation.staticcall( + abi.encodeWithSelector( + this.validateHashSignature.selector, + hash, + signer, + signature + ) + ); } /// @dev Validates a hash-only signature type. Low-level, hidden variant. diff --git a/contracts/zero-ex/contracts/src/features/TransformERC20.sol b/contracts/zero-ex/contracts/src/features/TransformERC20.sol index 85b4b1b651..e7bafaa355 100644 --- a/contracts/zero-ex/contracts/src/features/TransformERC20.sol +++ b/contracts/zero-ex/contracts/src/features/TransformERC20.sol @@ -31,9 +31,11 @@ import "../external/FlashWallet.sol"; import "../storage/LibTransformERC20Storage.sol"; import "../transformers/IERC20Transformer.sol"; import "../transformers/LibERC20Transformer.sol"; +import "./libs/LibSignedCallData.sol"; import "./ITransformERC20.sol"; import "./ITokenSpender.sol"; import "./IFeature.sol"; +import "./ISignatureValidator.sol"; import "./ISimpleFunctionRegistry.sol"; @@ -43,6 +45,8 @@ contract TransformERC20 is ITransformERC20, FixinCommon { + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; /// @dev Stack vars for `_transformERC20Private()`. struct TransformERC20PrivateState { @@ -55,10 +59,7 @@ contract TransformERC20 is /// @dev Name of this feature. string public constant override FEATURE_NAME = "TransformERC20"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0); - - using LibSafeMathV06 for uint256; - using LibRichErrorsV06 for bytes; + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 2, 0); constructor() public FixinCommon() { // solhint-disable-next-line no-empty-blocks @@ -76,6 +77,8 @@ contract TransformERC20 is _registerFeatureFunction(this.createTransformWallet.selector); _registerFeatureFunction(this.getTransformWallet.selector); _registerFeatureFunction(this.setTransformerDeployer.selector); + _registerFeatureFunction(this.setQuoteSigner.selector); + _registerFeatureFunction(this.getQuoteSigner.selector); _registerFeatureFunction(this.transformERC20.selector); _registerFeatureFunction(this._transformERC20.selector); this.createTransformWallet(); @@ -95,6 +98,18 @@ contract TransformERC20 is emit TransformerDeployerUpdated(transformerDeployer); } + /// @dev Replace the optional signer for `transformERC20()` calldata. + /// Only callable by the owner. + /// @param quoteSigner The address of the new calldata signer. + function setQuoteSigner(address quoteSigner) + external + override + onlyOwner + { + LibTransformERC20Storage.getStorage().quoteSigner = quoteSigner; + emit QuoteSignerUpdated(quoteSigner); + } + /// @dev Return the allowed deployer for transformers. /// @return deployer The transform deployer address. function getTransformerDeployer() @@ -106,6 +121,17 @@ contract TransformERC20 is return LibTransformERC20Storage.getStorage().transformerDeployer; } + /// @dev Return the optional signer for `transformERC20()` calldata. + /// @return signer The signer address. + function getQuoteSigner() + public + override + view + returns (address signer) + { + return LibTransformERC20Storage.getStorage().quoteSigner; + } + /// @dev Deploy a new wallet instance and replace the current one with it. /// Useful if we somehow break the current wallet instance. /// Only callable by the owner. @@ -147,42 +173,26 @@ contract TransformERC20 is payable returns (uint256 outputTokenAmount) { + (bytes32 callDataHash, bytes memory callDataSignature) = + LibSignedCallData.parseCallData(msg.data); return _transformERC20Private( - keccak256(msg.data), - msg.sender, - inputToken, - outputToken, - inputTokenAmount, - minOutputTokenAmount, - transformations + TransformERC20Args({ + taker: msg.sender, + inputToken: inputToken, + outputToken: outputToken, + inputTokenAmount: inputTokenAmount, + minOutputTokenAmount: minOutputTokenAmount, + transformations: transformations, + callDataHash: callDataHash, + callDataSignature: callDataSignature + }) ); } /// @dev Internal version of `transformERC20()`. Only callable from within. - /// @param callDataHash Hash of the ingress calldata. - /// @param taker The taker address. - /// @param inputToken The token being provided by the taker. - /// If `0xeee...`, ETH is implied and should be provided with the call.` - /// @param outputToken The token to be acquired by the taker. - /// `0xeee...` implies ETH. - /// @param inputTokenAmount The amount of `inputToken` to take from the taker. - /// If set to `uint256(-1)`, the entire spendable balance of the taker - /// will be solt. - /// @param minOutputTokenAmount The minimum amount of `outputToken` the taker - /// must receive for the entire transformation to succeed. If set to zero, - /// the minimum output token transfer will not be asserted. - /// @param transformations The transformations to execute on the token balance(s) - /// in sequence. + /// @param args A `TransformERC20Args` struct. /// @return outputTokenAmount The amount of `outputToken` received by the taker. - function _transformERC20( - bytes32 callDataHash, - address payable taker, - IERC20TokenV06 inputToken, - IERC20TokenV06 outputToken, - uint256 inputTokenAmount, - uint256 minOutputTokenAmount, - Transformation[] memory transformations - ) + function _transformERC20(TransformERC20Args memory args) public virtual override @@ -190,50 +200,21 @@ contract TransformERC20 is onlySelf returns (uint256 outputTokenAmount) { - return _transformERC20Private( - callDataHash, - taker, - inputToken, - outputToken, - inputTokenAmount, - minOutputTokenAmount, - transformations - ); + return _transformERC20Private(args); } /// @dev Private version of `transformERC20()`. - /// @param callDataHash Hash of the ingress calldata. - /// @param taker The taker address. - /// @param inputToken The token being provided by the taker. - /// If `0xeee...`, ETH is implied and should be provided with the call.` - /// @param outputToken The token to be acquired by the taker. - /// `0xeee...` implies ETH. - /// @param inputTokenAmount The amount of `inputToken` to take from the taker. - /// If set to `uint256(-1)`, the entire spendable balance of the taker - /// will be solt. - /// @param minOutputTokenAmount The minimum amount of `outputToken` the taker - /// must receive for the entire transformation to succeed. If set to zero, - /// the minimum output token transfer will not be asserted. - /// @param transformations The transformations to execute on the token balance(s) - /// in sequence. + /// @param args A `TransformERC20Args` struct. /// @return outputTokenAmount The amount of `outputToken` received by the taker. - function _transformERC20Private( - bytes32 callDataHash, - address payable taker, - IERC20TokenV06 inputToken, - IERC20TokenV06 outputToken, - uint256 inputTokenAmount, - uint256 minOutputTokenAmount, - Transformation[] memory transformations - ) + function _transformERC20Private(TransformERC20Args memory args) private returns (uint256 outputTokenAmount) { // If the input token amount is -1, transform the taker's entire // spendable balance. - if (inputTokenAmount == uint256(-1)) { - inputTokenAmount = ITokenSpender(address(this)) - .getSpendableERC20BalanceOf(inputToken, taker); + if (args.inputTokenAmount == uint256(-1)) { + args.inputTokenAmount = ITokenSpender(address(this)) + .getSpendableERC20BalanceOf(args.inputToken, args.taker); } TransformERC20PrivateState memory state; @@ -242,55 +223,65 @@ contract TransformERC20 is // Remember the initial output token balance of the taker. state.takerOutputTokenBalanceBefore = - LibERC20Transformer.getTokenBalanceOf(outputToken, taker); + LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker); // Pull input tokens from the taker to the wallet and transfer attached ETH. _transferInputTokensAndAttachedEth( - inputToken, - taker, + args.inputToken, + args.taker, address(state.wallet), - inputTokenAmount + args.inputTokenAmount ); - // Perform transformations. - for (uint256 i = 0; i < transformations.length; ++i) { - _executeTransformation( - state.wallet, - transformations[i], - state.transformerDeployer, - taker, - callDataHash + { + // Validate that the calldata was signed by the quote signer. + // `validCallDataHash` will be 0x0 if not. + bytes32 validCallDataHash = _getValidCallDataHash( + args.callDataHash, + args.callDataSignature ); + // Perform transformations. + for (uint256 i = 0; i < args.transformations.length; ++i) { + _executeTransformation( + state.wallet, + args.transformations[i], + state.transformerDeployer, + args.taker, + // Transformers will receive a null calldata hash if + // the calldata was not properly signed. + validCallDataHash + ); + } } // Compute how much output token has been transferred to the taker. state.takerOutputTokenBalanceAfter = - LibERC20Transformer.getTokenBalanceOf(outputToken, taker); + LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker); if (state.takerOutputTokenBalanceAfter > state.takerOutputTokenBalanceBefore) { outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub( state.takerOutputTokenBalanceBefore ); } else if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) { LibTransformERC20RichErrors.NegativeTransformERC20OutputError( - address(outputToken), + address(args.outputToken), state.takerOutputTokenBalanceBefore - state.takerOutputTokenBalanceAfter ).rrevert(); } // Ensure enough output token has been sent to the taker. - if (outputTokenAmount < minOutputTokenAmount) { + if (outputTokenAmount < args.minOutputTokenAmount) { LibTransformERC20RichErrors.IncompleteTransformERC20Error( - address(outputToken), + address(args.outputToken), outputTokenAmount, - minOutputTokenAmount + args.minOutputTokenAmount ).rrevert(); } // Emit an event. emit TransformedERC20( - taker, - address(inputToken), - address(outputToken), - inputTokenAmount, + args.taker, + address(args.inputToken), + address(args.outputToken), + args.inputTokenAmount, outputTokenAmount ); } @@ -385,4 +376,29 @@ contract TransformERC20 is ).rrevert(); } } + + /// @dev Check if a call data hash is signed by the quote signer. + /// @param callDataHash The hash of the callData. + /// @param signature The signature provided by `getQuoteSigner()`. + /// @return validCallDataHash `callDataHash` if so and `0x0` otherwise. + function _getValidCallDataHash( + bytes32 callDataHash, + bytes memory signature + ) + private + view + returns (bytes32 validCallDataHash) + { + if (signature.length == 0) { + return bytes32(0); + } + + if (ISignatureValidator(address(this)).isValidHashSignature( + callDataHash, + getQuoteSigner(), + signature + )) { + return callDataHash; + } + } } diff --git a/contracts/zero-ex/contracts/src/features/libs/LibSignedCallData.sol b/contracts/zero-ex/contracts/src/features/libs/LibSignedCallData.sol new file mode 100644 index 0000000000..8a58487d56 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/libs/LibSignedCallData.sol @@ -0,0 +1,67 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; + + +/// @dev Library for working with signed calldata. +library LibSignedCallData { + using LibBytesV06 for bytes; + + // bytes4(keccak256('SignedCallDataSignature(bytes)')) + bytes4 constant private SIGNATURE_SELECTOR = 0xf86d1d92; + + /// @dev Try to parse potentially signed calldata into its hash and signature + /// components. Signed calldata has signature data appended to it. + /// @param callData the raw call data. + /// @return callDataHash The hash of the signed callData, or 0x0 if no signature + /// is present. + /// @return signature The signature bytes, if present. + function parseCallData(bytes memory callData) + internal + pure + returns (bytes32 callDataHash, bytes memory signature) + { + // Signed calldata has a 70 byte signature appended as: + // ``` + // abi.encodePacked( + // callData, + // bytes4(keccak256('SignedCallDataSignature(bytes)')), + // signature // 66 bytes + // ); + // ``` + + if ( + // Signed callData has to be at least 70 bytes long. + callData.length < 70 || + // The bytes4 at offset -70 should equal `SIGNATURE_SELECTOR`. + SIGNATURE_SELECTOR != callData.readBytes4(callData.length - 70) + ) { + return (keccak256(callData), signature); + } + // Consider everything before the signature selector as the original + // calldata and everything after as the signature. + assembly { + callDataHash := keccak256(add(callData, 32), sub(mload(callData), 70)) + } + signature = callData.slice(callData.length - 66, callData.length); + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol b/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol index 95f22ccbdc..441898ef7f 100644 --- a/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol @@ -32,6 +32,8 @@ library LibTransformERC20Storage { IFlashWallet wallet; // The transformer deployer address. address transformerDeployer; + // The optional signer for `transformERC20()` calldata. + address quoteSigner; } /// @dev Get the storage bucket for this contract. diff --git a/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol index 7e653068b7..913fa414d7 100644 --- a/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol @@ -28,24 +28,17 @@ contract TestMetaTransactionsTransformERC20Feature is event TransformERC20Called( address sender, uint256 value, - bytes32 callDataHash, address taker, IERC20TokenV06 inputToken, IERC20TokenV06 outputToken, uint256 inputTokenAmount, uint256 minOutputTokenAmount, - Transformation[] transformations + Transformation[] transformations, + bytes32 callDataHash, + bytes callDataSignature ); - function _transformERC20( - bytes32 callDataHash, - address payable taker, - IERC20TokenV06 inputToken, - IERC20TokenV06 outputToken, - uint256 inputTokenAmount, - uint256 minOutputTokenAmount, - Transformation[] memory transformations - ) + function _transformERC20(TransformERC20Args memory args) public override payable @@ -58,13 +51,14 @@ contract TestMetaTransactionsTransformERC20Feature is emit TransformERC20Called( msg.sender, msg.value, - callDataHash, - taker, - inputToken, - outputToken, - inputTokenAmount, - minOutputTokenAmount, - transformations + args.taker, + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + args.callDataHash, + args.callDataSignature ); return 1337; } diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index 4af4cae0b9..b55ce502b3 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -45,3 +45,4 @@ export { export * from './nonce_utils'; export * from './migration'; +export * from './signed_call_data'; diff --git a/contracts/zero-ex/src/signed_call_data.ts b/contracts/zero-ex/src/signed_call_data.ts new file mode 100644 index 0000000000..49a489a207 --- /dev/null +++ b/contracts/zero-ex/src/signed_call_data.ts @@ -0,0 +1,29 @@ +import { SignatureType } from '@0x/types'; +import { hexUtils } from '@0x/utils'; +import * as ethjs from 'ethereumjs-util'; + +/** + * Generate calldata with a signature appended. + */ +export function signCallData(callData: string, privateKey: string): string { + const prefix = ethjs.sha3('SignedCallDataSignature(bytes)').slice(0, 4); + return hexUtils.concat(callData, prefix, generateCallDataSignature(callData, privateKey)); +} + +/** + * Generate a signature for calldata. + */ +export function generateCallDataSignature(callData: string, privateKey: string): string { + return generateCallDataHashSignature(hexUtils.hash(callData), privateKey); +} + +/** + * Generate a signature for calldata hash. + */ +export function generateCallDataHashSignature(callDataHash: string, privateKey: string): string { + const { r, s, v } = ethjs.ecsign( + ethjs.hashPersonalMessage(ethjs.toBuffer(callDataHash)), + ethjs.toBuffer(privateKey), + ); + return hexUtils.concat(v, r, s, SignatureType.EthSign); +} diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 6750466ef4..bc376b84f4 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -41,6 +41,7 @@ import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorag import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; import * as LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json'; +import * as LibSignedCallData from '../test/generated-artifacts/LibSignedCallData.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json'; @@ -113,6 +114,7 @@ export const artifacts = { SimpleFunctionRegistry: SimpleFunctionRegistry as ContractArtifact, TokenSpender: TokenSpender as ContractArtifact, TransformERC20: TransformERC20 as ContractArtifact, + LibSignedCallData: LibSignedCallData as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, FixinEIP712: FixinEIP712 as ContractArtifact, FixinGasToken: FixinGasToken as ContractArtifact, diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts index 35020bf632..f5f30fda7f 100644 --- a/contracts/zero-ex/test/features/meta_transactions_test.ts +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -11,6 +11,7 @@ import { ExchangeProxyMetaTransaction } from '@0x/types'; import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; +import { generateCallDataSignature, signCallData } from '../../src/signed_call_data'; import { MetaTransactionsContract, ZeroExContract } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; @@ -22,7 +23,7 @@ import { TestMintableERC20TokenContract, } from '../wrappers'; -const { NULL_ADDRESS, ZERO_AMOUNT } = constants; +const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants; blockchainTests.resets('MetaTransactions feature', env => { let owner: string; @@ -161,6 +162,50 @@ blockchainTests.resets('MetaTransactions feature', env => { value: mtx.value, callDataHash: hexUtils.hash(mtx.callData), taker: mtx.signer, + callDataSignature: NULL_BYTES, + }, + ], + TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called, + ); + }); + + it('can call `TransformERC20.transformERC20()` with signed calldata', async () => { + const args = getRandomTransformERC20Args(); + const callData = transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(); + const callDataSignerKey = hexUtils.random(); + const callDataSignature = generateCallDataSignature(callData, callDataSignerKey); + const signedCallData = signCallData(callData, callDataSignerKey); + const mtx = getRandomMetaTransaction({ callData: signedCallData }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); + expect(rawResult).to.eq(RAW_SUCCESS_RESULT); + const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + verifyEventsFromLogs( + receipt.logs, + [ + { + inputToken: args.inputToken, + outputToken: args.outputToken, + inputTokenAmount: args.inputTokenAmount, + minOutputTokenAmount: args.minOutputTokenAmount, + transformations: args.transformations, + sender: zeroEx.address, + value: mtx.value, + callDataHash: hexUtils.hash(callData), + taker: mtx.signer, + callDataSignature, }, ], TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called, @@ -237,15 +282,16 @@ blockchainTests.resets('MetaTransactions feature', env => { }; const tx = feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); const actualCallData = transformERC20Feature - ._transformERC20( - hexUtils.hash(mtx.callData), - mtx.signer, - args.inputToken, - args.outputToken, - args.inputTokenAmount, - args.minOutputTokenAmount, - args.transformations, - ) + ._transformERC20({ + taker: mtx.signer, + inputToken: args.inputToken, + outputToken: args.outputToken, + inputTokenAmount: args.inputTokenAmount, + minOutputTokenAmount: args.minOutputTokenAmount, + transformations: args.transformations, + callDataHash: hexUtils.hash(mtx.callData), + callDataSignature: NULL_BYTES, + }) .getABIEncodedTransactionData(); return expect(tx).to.revertWith( new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError( diff --git a/contracts/zero-ex/test/features/transform_erc20_test.ts b/contracts/zero-ex/test/features/transform_erc20_test.ts index a7af7ecd96..2841e623f8 100644 --- a/contracts/zero-ex/test/features/transform_erc20_test.ts +++ b/contracts/zero-ex/test/features/transform_erc20_test.ts @@ -10,7 +10,10 @@ import { } from '@0x/contracts-test-utils'; import { ETH_TOKEN_ADDRESS } from '@0x/order-utils'; import { AbiEncoder, hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils'; +import { DecodedLogEntry } from 'ethereum-types'; +import * as ethjs from 'ethereumjs-util'; +import { generateCallDataHashSignature, signCallData } from '../../src/signed_call_data'; import { TransformERC20Contract, ZeroExContract } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; @@ -21,10 +24,17 @@ import { TestMintableERC20TokenContract, TestMintTokenERC20TransformerContract, TestMintTokenERC20TransformerEvents, + TestMintTokenERC20TransformerMintTransformEventArgs, TransformERC20Events, } from '../wrappers'; +const { NULL_BYTES, NULL_BYTES32 } = constants; + +type MintTokenTransformerEvent = DecodedLogEntry; + blockchainTests.resets('TransformERC20 feature', env => { + const callDataSignerKey = hexUtils.random(); + const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey))); let owner: string; let taker: string; let transformerDeployer: string; @@ -54,6 +64,7 @@ blockchainTests.resets('TransformERC20 feature', env => { allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults) .getAllowanceTarget() .callAsync(); + await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync(); }); const { MAX_UINT256, ZERO_AMOUNT } = constants; @@ -101,7 +112,29 @@ blockchainTests.resets('TransformERC20 feature', env => { }); }); - describe('_transformERC20()', () => { + describe('quote signer', () => { + it('`getQuoteSigner()` returns the quote signer', async () => { + const actualSigner = await feature.getQuoteSigner().callAsync(); + expect(actualSigner).to.eq(callDataSigner); + }); + + it('owner can set the quote signer with `setQuoteSigner()`', async () => { + const newSigner = randomAddress(); + const receipt = await feature.setQuoteSigner(newSigner).awaitTransactionSuccessAsync({ from: owner }); + verifyEventsFromLogs(receipt.logs, [{ quoteSigner: newSigner }], TransformERC20Events.QuoteSignerUpdated); + const actualSigner = await feature.getQuoteSigner().callAsync(); + expect(actualSigner).to.eq(newSigner); + }); + + it('non-owner cannot set the transformer deployer with `setTransformerDeployer()`', async () => { + const newSigner = randomAddress(); + const notOwner = randomAddress(); + const tx = feature.setQuoteSigner(newSigner).callAsync({ from: notOwner }); + return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner)); + }); + }); + + describe('_transformERC20()/transformERC20()', () => { let inputToken: TestMintableERC20TokenContract; let outputToken: TestMintableERC20TokenContract; let mintTransformer: TestMintTokenERC20TransformerContract; @@ -187,325 +220,552 @@ blockchainTests.resets('TransformERC20 feature', env => { }; } - it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount", async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(0, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(1, '1e18'); - const outputTokenMintAmount = minOutputTokenAmount; - const callValue = getRandomInteger(1, '1e18'); - const callDataHash = hexUtils.random(); - const transformation = createMintTokenTransformation({ - outputTokenMintAmount, - inputTokenBurnAmunt: inputTokenAmount, - }); - const receipt = await feature - ._transformERC20( - callDataHash, - taker, - inputToken.address, - outputToken.address, - inputTokenAmount, - minOutputTokenAmount, - [transformation], - ) - .awaitTransactionSuccessAsync({ value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { + describe('_transformERC20()', () => { + it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount", async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const receipt = await feature + ._transformERC20({ taker, - inputTokenAmount, - outputTokenAmount: outputTokenMintAmount, inputToken: inputToken.address, outputToken: outputToken.address, - }, - ], - TransformERC20Events.TransformedERC20, - ); - verifyEventsFromLogs( - receipt.logs, - [ - { - callDataHash, - taker, - context: wallet.address, - caller: zeroEx.address, - data: transformation.data, - inputTokenBalance: inputTokenAmount, - ethBalance: callValue, - }, - ], - TestMintTokenERC20TransformerEvents.MintTransform, - ); - }); - - it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => { - const startingInputTokenBalance = getRandomInteger(0, '100e18'); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(1, '1e18'); - const outputTokenMintAmount = minOutputTokenAmount; - const callValue = outputTokenMintAmount.times(2); - const callDataHash = hexUtils.random(); - const transformation = createMintTokenTransformation({ - outputTokenMintAmount, - inputTokenBurnAmunt: inputTokenAmount, - outputTokenAddress: ETH_TOKEN_ADDRESS, - }); - const startingOutputTokenBalance = await env.web3Wrapper.getBalanceInWeiAsync(taker); - const receipt = await feature - ._transformERC20( - callDataHash, - taker, - inputToken.address, - ETH_TOKEN_ADDRESS, - inputTokenAmount, - minOutputTokenAmount, - [transformation], - ) - .awaitTransactionSuccessAsync({ value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, inputTokenAmount, - outputTokenAmount: outputTokenMintAmount, + minOutputTokenAmount, + transformations: [transformation], + callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + taker, + inputTokenAmount, + outputTokenAmount: outputTokenMintAmount, + inputToken: inputToken.address, + outputToken: outputToken.address, + }, + ], + TransformERC20Events.TransformedERC20, + ); + verifyEventsFromLogs( + receipt.logs, + [ + { + callDataHash: NULL_BYTES32, + taker, + context: wallet.address, + caller: zeroEx.address, + data: transformation.data, + inputTokenBalance: inputTokenAmount, + ethBalance: callValue, + }, + ], + TestMintTokenERC20TransformerEvents.MintTransform, + ); + }); + + it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => { + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = outputTokenMintAmount.times(2); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + outputTokenAddress: ETH_TOKEN_ADDRESS, + }); + const startingOutputTokenBalance = await env.web3Wrapper.getBalanceInWeiAsync(taker); + const receipt = await feature + ._transformERC20({ + taker, inputToken: inputToken.address, outputToken: ETH_TOKEN_ADDRESS, - }, - ], - TransformERC20Events.TransformedERC20, - ); - verifyEventsFromLogs( - receipt.logs, - [ - { + inputTokenAmount, + minOutputTokenAmount, + transformations: [transformation], callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + taker, + inputTokenAmount, + outputTokenAmount: outputTokenMintAmount, + inputToken: inputToken.address, + outputToken: ETH_TOKEN_ADDRESS, + }, + ], + TransformERC20Events.TransformedERC20, + ); + verifyEventsFromLogs( + receipt.logs, + [ + { + callDataHash: NULL_BYTES32, + taker, + context: wallet.address, + caller: zeroEx.address, + data: transformation.data, + inputTokenBalance: inputTokenAmount, + ethBalance: callValue, + }, + ], + TestMintTokenERC20TransformerEvents.MintTransform, + ); + expect(await env.web3Wrapper.getBalanceInWeiAsync(taker)).to.bignumber.eq( + startingOutputTokenBalance.plus(outputTokenMintAmount), + ); + }); + + it("succeeds if taker's output token balance increases by more than minOutputTokenAmount", async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount.plus(1); + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const receipt = await feature + ._transformERC20({ taker, - context: wallet.address, - caller: zeroEx.address, - data: transformation.data, - inputTokenBalance: inputTokenAmount, - ethBalance: callValue, - }, - ], - TestMintTokenERC20TransformerEvents.MintTransform, - ); - expect(await env.web3Wrapper.getBalanceInWeiAsync(taker)).to.bignumber.eq( - startingOutputTokenBalance.plus(outputTokenMintAmount), - ); + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations: [transformation], + callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + taker, + inputTokenAmount, + outputTokenAmount: outputTokenMintAmount, + inputToken: inputToken.address, + outputToken: outputToken.address, + }, + ], + TransformERC20Events.TransformedERC20, + ); + verifyEventsFromLogs( + receipt.logs, + [ + { + callDataHash: NULL_BYTES32, + taker, + context: wallet.address, + caller: zeroEx.address, + data: transformation.data, + inputTokenBalance: inputTokenAmount, + ethBalance: callValue, + }, + ], + TestMintTokenERC20TransformerEvents.MintTransform, + ); + }); + + it("throws if taker's output token balance increases by less than minOutputTokenAmount", async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount.minus(1); + const callValue = getRandomInteger(1, '1e18'); + const tx = feature + ._transformERC20({ + callDataHash: hexUtils.random(), + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + callDataSignature: NULL_BYTES, + transformations: [ + createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }), + ], + }) + .awaitTransactionSuccessAsync({ value: callValue }); + const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error( + outputToken.address, + outputTokenMintAmount, + minOutputTokenAmount, + ); + return expect(tx).to.revertWith(expectedError); + }); + + it("throws if taker's output token balance decreases", async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = ZERO_AMOUNT; + const outputTokenFeeAmount = 1; + const callValue = getRandomInteger(1, '1e18'); + const tx = feature + ._transformERC20({ + callDataHash: hexUtils.random(), + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + callDataSignature: NULL_BYTES, + transformations: [ + createMintTokenTransformation({ + outputTokenFeeAmount, + inputTokenBurnAmunt: inputTokenAmount, + }), + ], + }) + .awaitTransactionSuccessAsync({ value: callValue }); + const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError( + outputToken.address, + outputTokenFeeAmount, + ); + return expect(tx).to.revertWith(expectedError); + }); + + it('can call multiple transformers', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(2, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(2, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + // Split the total minting between two transformers. + const transformations = [ + createMintTokenTransformation({ + inputTokenBurnAmunt: 1, + outputTokenMintAmount: 1, + }), + createMintTokenTransformation({ + inputTokenBurnAmunt: inputTokenAmount.minus(1), + outputTokenMintAmount: outputTokenMintAmount.minus(1), + }), + ]; + const receipt = await feature + ._transformERC20({ + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations, + callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + callDataHash: NULL_BYTES32, + taker, + context: wallet.address, + caller: zeroEx.address, + data: transformations[0].data, + inputTokenBalance: inputTokenAmount, + ethBalance: callValue, + }, + { + callDataHash: NULL_BYTES32, + taker, + context: wallet.address, + caller: zeroEx.address, + data: transformations[1].data, + inputTokenBalance: inputTokenAmount.minus(1), + ethBalance: callValue, + }, + ], + TestMintTokenERC20TransformerEvents.MintTransform, + ); + }); + + it('fails with invalid transformer nonce', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(2, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(2, '1e18'); + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformations = [createMintTokenTransformation({ deploymentNonce: 1337 })]; + const tx = feature + ._transformERC20({ + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations, + callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.TransformerFailedError( + undefined, + transformations[0].data, + constants.NULL_BYTES, + ), + ); + }); + + it('passes the calldata hash to transformer with proper signature', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const receipt = await feature + ._transformERC20({ + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations: [transformation], + callDataHash, + callDataSignature: generateCallDataHashSignature(callDataHash, callDataSignerKey), + }) + .awaitTransactionSuccessAsync({ value: callValue }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(callDataHash); + }); + + it('passes empty calldata hash to transformer with improper signature', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const receipt = await feature + ._transformERC20({ + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations: [transformation], + callDataHash, + callDataSignature: generateCallDataHashSignature(callDataHash, hexUtils.random()), + }) + .awaitTransactionSuccessAsync({ value: callValue }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(NULL_BYTES32); + }); + + it('passes empty calldata hash to transformer with no signature', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const callDataHash = hexUtils.random(); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const receipt = await feature + ._transformERC20({ + taker, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenAmount, + minOutputTokenAmount, + transformations: [transformation], + callDataHash, + callDataSignature: NULL_BYTES, + }) + .awaitTransactionSuccessAsync({ value: callValue }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(NULL_BYTES32); + }); }); - it("succeeds if taker's output token balance increases by more than minOutputTokenAmount", async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(0, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(1, '1e18'); - const outputTokenMintAmount = minOutputTokenAmount.plus(1); - const callValue = getRandomInteger(1, '1e18'); - const callDataHash = hexUtils.random(); - const transformation = createMintTokenTransformation({ - outputTokenMintAmount, - inputTokenBurnAmunt: inputTokenAmount, - }); - const receipt = await feature - ._transformERC20( - callDataHash, - taker, + describe('transformERC20()', () => { + it('passes the calldata hash to transformer with properly signed calldata', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const bakedCall = feature.transformERC20( inputToken.address, outputToken.address, inputTokenAmount, minOutputTokenAmount, [transformation], - ) - .awaitTransactionSuccessAsync({ value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - taker, - inputTokenAmount, - outputTokenAmount: outputTokenMintAmount, - inputToken: inputToken.address, - outputToken: outputToken.address, - }, - ], - TransformERC20Events.TransformedERC20, - ); - verifyEventsFromLogs( - receipt.logs, - [ - { - callDataHash, - taker, - context: wallet.address, - caller: zeroEx.address, - data: transformation.data, - inputTokenBalance: inputTokenAmount, - ethBalance: callValue, - }, - ], - TestMintTokenERC20TransformerEvents.MintTransform, - ); - }); + ); + const callData = bakedCall.getABIEncodedTransactionData(); + const signedCallData = signCallData(callData, callDataSignerKey); + const receipt = await bakedCall.awaitTransactionSuccessAsync({ + from: taker, + value: callValue, + data: signedCallData, + }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(hexUtils.hash(callData)); + }); - it("throws if taker's output token balance increases by less than minOutputTokenAmount", async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(0, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(1, '1e18'); - const outputTokenMintAmount = minOutputTokenAmount.minus(1); - const callValue = getRandomInteger(1, '1e18'); - const tx = feature - ._transformERC20( - hexUtils.random(), - taker, + it('passes empty calldata hash to transformer with improperly signed calldata', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const bakedCall = feature.transformERC20( inputToken.address, outputToken.address, inputTokenAmount, minOutputTokenAmount, - [ - createMintTokenTransformation({ - outputTokenMintAmount, - inputTokenBurnAmunt: inputTokenAmount, - }), - ], - ) - .awaitTransactionSuccessAsync({ value: callValue }); - const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error( - outputToken.address, - outputTokenMintAmount, - minOutputTokenAmount, - ); - return expect(tx).to.revertWith(expectedError); - }); + [transformation], + ); + const callData = bakedCall.getABIEncodedTransactionData(); + const signedCallData = signCallData(callData, hexUtils.random()); + const receipt = await bakedCall.awaitTransactionSuccessAsync({ + from: taker, + value: callValue, + data: signedCallData, + }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(NULL_BYTES32); + }); - it("throws if taker's output token balance decreases", async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(0, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = ZERO_AMOUNT; - const outputTokenFeeAmount = 1; - const callValue = getRandomInteger(1, '1e18'); - const tx = feature - ._transformERC20( - hexUtils.random(), - taker, + it('passes empty calldata hash to transformer with unsigned calldata', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const bakedCall = feature.transformERC20( inputToken.address, outputToken.address, inputTokenAmount, minOutputTokenAmount, - [ - createMintTokenTransformation({ - outputTokenFeeAmount, - inputTokenBurnAmunt: inputTokenAmount, - }), - ], - ) - .awaitTransactionSuccessAsync({ value: callValue }); - const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError( - outputToken.address, - outputTokenFeeAmount, - ); - return expect(tx).to.revertWith(expectedError); - }); + [transformation], + ); + const callData = bakedCall.getABIEncodedTransactionData(); + const receipt = await bakedCall.awaitTransactionSuccessAsync({ + from: taker, + value: callValue, + data: callData, + }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(NULL_BYTES32); + }); - it('can call multiple transformers', async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(2, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(2, '1e18'); - const outputTokenMintAmount = minOutputTokenAmount; - const callValue = getRandomInteger(1, '1e18'); - const callDataHash = hexUtils.random(); - // Split the total minting between two transformers. - const transformations = [ - createMintTokenTransformation({ - inputTokenBurnAmunt: 1, - outputTokenMintAmount: 1, - }), - createMintTokenTransformation({ - inputTokenBurnAmunt: inputTokenAmount.minus(1), - outputTokenMintAmount: outputTokenMintAmount.minus(1), - }), - ]; - const receipt = await feature - ._transformERC20( - callDataHash, - taker, + it('passes empty calldata hash to transformer with calldata with malformed signature', async () => { + const startingOutputTokenBalance = getRandomInteger(0, '100e18'); + const startingInputTokenBalance = getRandomInteger(0, '100e18'); + await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); + await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); + const inputTokenAmount = getRandomPortion(startingInputTokenBalance); + const minOutputTokenAmount = getRandomInteger(1, '1e18'); + const outputTokenMintAmount = minOutputTokenAmount; + const callValue = getRandomInteger(1, '1e18'); + const transformation = createMintTokenTransformation({ + outputTokenMintAmount, + inputTokenBurnAmunt: inputTokenAmount, + }); + const bakedCall = feature.transformERC20( inputToken.address, outputToken.address, inputTokenAmount, minOutputTokenAmount, - transformations, - ) - .awaitTransactionSuccessAsync({ value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - callDataHash, - taker, - context: wallet.address, - caller: zeroEx.address, - data: transformations[0].data, - inputTokenBalance: inputTokenAmount, - ethBalance: callValue, - }, - { - callDataHash, - taker, - context: wallet.address, - caller: zeroEx.address, - data: transformations[1].data, - inputTokenBalance: inputTokenAmount.minus(1), - ethBalance: callValue, - }, - ], - TestMintTokenERC20TransformerEvents.MintTransform, - ); - }); - - it('fails with invalid transformer nonce', async () => { - const startingOutputTokenBalance = getRandomInteger(0, '100e18'); - const startingInputTokenBalance = getRandomInteger(2, '100e18'); - await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync(); - await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); - const inputTokenAmount = getRandomPortion(startingInputTokenBalance); - const minOutputTokenAmount = getRandomInteger(2, '1e18'); - const callValue = getRandomInteger(1, '1e18'); - const callDataHash = hexUtils.random(); - const transformations = [createMintTokenTransformation({ deploymentNonce: 1337 })]; - const tx = feature - ._transformERC20( - callDataHash, - taker, - inputToken.address, - outputToken.address, - inputTokenAmount, - minOutputTokenAmount, - transformations, - ) - .awaitTransactionSuccessAsync({ value: callValue }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.TransformerFailedError( - undefined, - transformations[0].data, - constants.NULL_BYTES, - ), - ); + [transformation], + ); + const callData = bakedCall.getABIEncodedTransactionData(); + const signedCallData = hexUtils.concat( + hexUtils.slice(signCallData(callData, hexUtils.random()), 0, -1), + 127, + ); + const receipt = await bakedCall.awaitTransactionSuccessAsync({ + from: taker, + value: callValue, + data: signedCallData, + }); + const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args; + expect(actualCallDataHash).to.eq(NULL_BYTES32); + }); }); }); }); diff --git a/contracts/zero-ex/test/full_migration_test.ts b/contracts/zero-ex/test/full_migration_test.ts index 107e4f6b28..dea5bf3749 100644 --- a/contracts/zero-ex/test/full_migration_test.ts +++ b/contracts/zero-ex/test/full_migration_test.ts @@ -81,6 +81,8 @@ blockchainTests.resets('Full migration', env => { 'createTransformWallet', 'getTransformWallet', 'setTransformerDeployer', + 'getQuoteSigner', + 'setQuoteSigner', ], }, SignatureValidator: { diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index b5827399e8..4b08a89f5e 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -39,6 +39,7 @@ export * from '../test/generated-wrappers/lib_ownable_storage'; export * from '../test/generated-wrappers/lib_proxy_rich_errors'; export * from '../test/generated-wrappers/lib_proxy_storage'; export * from '../test/generated-wrappers/lib_signature_rich_errors'; +export * from '../test/generated-wrappers/lib_signed_call_data'; export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors'; export * from '../test/generated-wrappers/lib_simple_function_registry_storage'; export * from '../test/generated-wrappers/lib_spender_rich_errors'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index caa5d8fca0..cdeac800eb 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -59,6 +59,7 @@ "test/generated-artifacts/LibProxyRichErrors.json", "test/generated-artifacts/LibProxyStorage.json", "test/generated-artifacts/LibSignatureRichErrors.json", + "test/generated-artifacts/LibSignedCallData.json", "test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json", "test/generated-artifacts/LibSimpleFunctionRegistryStorage.json", "test/generated-artifacts/LibSpenderRichErrors.json", diff --git a/packages/abi-gen/CHANGELOG.json b/packages/abi-gen/CHANGELOG.json index 2b566ec851..4350d22d58 100644 --- a/packages/abi-gen/CHANGELOG.json +++ b/packages/abi-gen/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "5.4.0", + "changes": [ + { + "note": "Allow overriding of `data` with contract calls and transactions", + "pr": 2626 + } + ] + }, { "timestamp": 1594788383, "version": "5.3.1", diff --git a/packages/abi-gen/templates/TypeScript/partials/method_call.handlebars b/packages/abi-gen/templates/TypeScript/partials/method_call.handlebars index 71910299e4..757906711b 100644 --- a/packages/abi-gen/templates/TypeScript/partials/method_call.handlebars +++ b/packages/abi-gen/templates/TypeScript/partials/method_call.handlebars @@ -8,10 +8,10 @@ async callAsync( if (self._deployedBytecodeIfExists) { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { - rawCallResult = await self._performCallAsync({ ...callData, data: this.getABIEncodedTransactionData() }, defaultBlock); + rawCallResult = await self._performCallAsync({ data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock); } {{else}} - const rawCallResult = await self._performCallAsync({ ...callData, data: this.getABIEncodedTransactionData() }, defaultBlock); + const rawCallResult = await self._performCallAsync({ data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock); {{/ifEquals}} const abiEncoder = self._lookupAbiEncoder(functionSignature); BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); diff --git a/packages/abi-gen/templates/TypeScript/partials/method_tx.handlebars b/packages/abi-gen/templates/TypeScript/partials/method_tx.handlebars index 25f3a27dba..763bf740bc 100644 --- a/packages/abi-gen/templates/TypeScript/partials/method_tx.handlebars +++ b/packages/abi-gen/templates/TypeScript/partials/method_tx.handlebars @@ -3,7 +3,7 @@ async sendTransactionAsync( opts: SendTransactionOpts = { shouldValidate: true }, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...txData }, this.estimateGasAsync.bind(this), ); if (opts.shouldValidate !== false) { @@ -21,7 +21,7 @@ async estimateGasAsync( txData?: Partial | undefined, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() } + { data: this.getABIEncodedTransactionData(), ...txData } ); return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); }, diff --git a/packages/abi-gen/test-cli/output/typescript/abi_gen_dummy.ts b/packages/abi-gen/test-cli/output/typescript/abi_gen_dummy.ts index 5cedf49268..5dd3fddaf0 100644 --- a/packages/abi-gen/test-cli/output/typescript/abi_gen_dummy.ts +++ b/packages/abi-gen/test-cli/output/typescript/abi_gen_dummy.ts @@ -997,7 +997,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1023,7 +1023,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1069,7 +1069,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1114,7 +1114,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1137,7 +1137,7 @@ export class AbiGenDummyContract extends BaseContract { opts: SendTransactionOpts = { shouldValidate: true }, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...txData }, this.estimateGasAsync.bind(this), ); if (opts.shouldValidate !== false) { @@ -1153,15 +1153,15 @@ export class AbiGenDummyContract extends BaseContract { }, async estimateGasAsync(txData?: Partial | undefined): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ - ...txData, data: this.getABIEncodedTransactionData(), + ...txData, }); return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); }, async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { BaseContract._assertCallParams(callData, defaultBlock); const rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); const abiEncoder = self._lookupAbiEncoder(functionSignature); @@ -1193,7 +1193,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1226,7 +1226,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1258,7 +1258,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1285,7 +1285,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1315,7 +1315,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1353,7 +1353,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1387,7 +1387,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1421,7 +1421,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1457,7 +1457,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1485,7 +1485,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1508,7 +1508,7 @@ export class AbiGenDummyContract extends BaseContract { opts: SendTransactionOpts = { shouldValidate: true }, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...txData }, this.estimateGasAsync.bind(this), ); if (opts.shouldValidate !== false) { @@ -1524,15 +1524,15 @@ export class AbiGenDummyContract extends BaseContract { }, async estimateGasAsync(txData?: Partial | undefined): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ - ...txData, data: this.getABIEncodedTransactionData(), + ...txData, }); return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); }, async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { BaseContract._assertCallParams(callData, defaultBlock); const rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); const abiEncoder = self._lookupAbiEncoder(functionSignature); @@ -1554,7 +1554,7 @@ export class AbiGenDummyContract extends BaseContract { opts: SendTransactionOpts = { shouldValidate: true }, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...txData }, this.estimateGasAsync.bind(this), ); if (opts.shouldValidate !== false) { @@ -1570,15 +1570,15 @@ export class AbiGenDummyContract extends BaseContract { }, async estimateGasAsync(txData?: Partial | undefined): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ - ...txData, data: this.getABIEncodedTransactionData(), + ...txData, }); return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); }, async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { BaseContract._assertCallParams(callData, defaultBlock); const rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); const abiEncoder = self._lookupAbiEncoder(functionSignature); @@ -1603,7 +1603,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1629,7 +1629,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1654,7 +1654,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1679,7 +1679,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1704,7 +1704,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1733,7 +1733,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1762,7 +1762,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1787,7 +1787,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1813,7 +1813,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1838,7 +1838,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1863,7 +1863,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1894,7 +1894,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1931,7 +1931,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1972,7 +1972,7 @@ export class AbiGenDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -1996,7 +1996,7 @@ export class AbiGenDummyContract extends BaseContract { opts: SendTransactionOpts = { shouldValidate: true }, ): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( - { ...txData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...txData }, this.estimateGasAsync.bind(this), ); if (opts.shouldValidate !== false) { @@ -2012,15 +2012,15 @@ export class AbiGenDummyContract extends BaseContract { }, async estimateGasAsync(txData?: Partial | undefined): Promise { const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ - ...txData, data: this.getABIEncodedTransactionData(), + ...txData, }); return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); }, async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { BaseContract._assertCallParams(callData, defaultBlock); const rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); const abiEncoder = self._lookupAbiEncoder(functionSignature); diff --git a/packages/abi-gen/test-cli/output/typescript/test_lib_dummy.ts b/packages/abi-gen/test-cli/output/typescript/test_lib_dummy.ts index 517c7bb5c9..3f6c20e62b 100644 --- a/packages/abi-gen/test-cli/output/typescript/test_lib_dummy.ts +++ b/packages/abi-gen/test-cli/output/typescript/test_lib_dummy.ts @@ -280,7 +280,7 @@ export class TestLibDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); } @@ -306,7 +306,7 @@ export class TestLibDummyContract extends BaseContract { rawCallResult = await self._evmExecAsync(this.getABIEncodedTransactionData()); } else { rawCallResult = await self._performCallAsync( - { ...callData, data: this.getABIEncodedTransactionData() }, + { data: this.getABIEncodedTransactionData(), ...callData }, defaultBlock, ); }