From a0c114573503375c4b45382ff3c9dcdc70cc94fc Mon Sep 17 00:00:00 2001 From: sudo rm -rf --no-preserve-root / Date: Wed, 10 Apr 2024 18:14:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20`Multicall`=20Modul?= =?UTF-8?q?e-Friendly=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🕓 Changelog This PR refactors the `Multicall` contract to make it module-friendly and ready for the breaking `0.4.0` release. --------- Signed-off-by: Pascal Marco Caversaccio --- .gas-snapshot | 18 ++--- CHANGELOG.md | 1 + README.md | 3 +- src/snekmate/utils/Multicall.vy | 50 ++++++------ src/snekmate/utils/mocks/MulticallMock.vy | 97 +++++++++++++++++++++++ test/utils/Multicall.t.sol | 5 +- 6 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 src/snekmate/utils/mocks/MulticallMock.vy diff --git a/.gas-snapshot b/.gas-snapshot index 93c11cc8..3a7aae42 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -602,15 +602,15 @@ MessageHashUtilsTest:testFuzzToTypedDataHash(string,string) (runs: 256, μ: 9948 MessageHashUtilsTest:testToDataWithIntendedValidatorHash() (gas: 11874) MessageHashUtilsTest:testToDataWithIntendedValidatorHashSelf() (gas: 12310) MessageHashUtilsTest:testToTypedDataHash() (gas: 8736) -MulticallTest:testMulticallRevert() (gas: 545382964) -MulticallTest:testMulticallSelfRevert() (gas: 1090186642) -MulticallTest:testMulticallSelfSuccess() (gas: 1635537407) -MulticallTest:testMulticallSuccess() (gas: 545390333) -MulticallTest:testMulticallValueRevertCase1() (gas: 545926302) -MulticallTest:testMulticallValueRevertCase2() (gas: 545933746) -MulticallTest:testMulticallValueSuccess() (gas: 545959264) -MulticallTest:testMultistaticcallRevert() (gas: 8937393460525252382) -MulticallTest:testMultistaticcallSuccess() (gas: 545354391) +MulticallTest:testMulticallRevert() (gas: 1151095) +MulticallTest:testMulticallSelfRevert() (gas: 2216002) +MulticallTest:testMulticallSelfSuccess() (gas: 3341496) +MulticallTest:testMulticallSuccess() (gas: 1160851) +MulticallTest:testMulticallValueRevertCase1() (gas: 1210261) +MulticallTest:testMulticallValueRevertCase2() (gas: 1211657) +MulticallTest:testMulticallValueSuccess() (gas: 1240219) +MulticallTest:testMultistaticcallRevert() (gas: 8937393460516748853) +MulticallTest:testMultistaticcallSuccess() (gas: 1129943) Ownable2StepInvariants:statefulFuzzOwner() (runs: 256, calls: 3840, reverts: 3840) Ownable2StepInvariants:statefulFuzzPendingOwner() (runs: 256, calls: 3840, reverts: 3840) Ownable2StepTest:testAcceptOwnershipNonPendingOwner() (gas: 47887) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8f6e07..e51e5fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - [`EIP712DomainSeparator`](https://github.com/pcaversaccio/snekmate/blob/v0.1.0/src/snekmate/utils/EIP712DomainSeparator.vy): Make `EIP712DomainSeparator` module-friendly. ([#229](https://github.com/pcaversaccio/snekmate/pull/229)) - [`Math`](https://github.com/pcaversaccio/snekmate/blob/v0.1.0/src/snekmate/utils/Math.vy): Make `Math` module-friendly. ([#230](https://github.com/pcaversaccio/snekmate/pull/230)) - [`MerkleProofVerification`](https://github.com/pcaversaccio/snekmate/blob/v0.1.0/src/snekmate/utils/MerkleProofVerification.vy): Make `MerkleProofVerification` module-friendly. ([#231](https://github.com/pcaversaccio/snekmate/pull/231)) + - [`Multicall`](https://github.com/pcaversaccio/snekmate/blob/v0.1.0/src/snekmate/utils/Multicall.vy): Make `Multicall` module-friendly. ([#232](https://github.com/pcaversaccio/snekmate/pull/232)) - **Vyper Contract Deployer** - [`VyperDeployer`](https://github.com/pcaversaccio/snekmate/blob/v0.1.0/lib/utils/VyperDeployer.sol): Improve error message in the event of a Vyper compilation error. ([#219](https://github.com/pcaversaccio/snekmate/pull/219)) diff --git a/README.md b/README.md index 14d29bdb..b424e280 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ src ├── SignatureCheckerMock — "SignatureChecker Module Reference Implementation" ├── EIP712DomainSeparatorMock — "EIP712DomainSeparator Module Reference Implementation" ├── MathMock — "Math Module Reference Implementation" - └── MerkleProofVerificationMock — "MerkleProofVerification Module Reference Implementation" + ├── MerkleProofVerificationMock — "MerkleProofVerification Module Reference Implementation" + └── MulticallMock — "Multicall Module Reference Implementation" ``` ## 🎛 Installation diff --git a/src/snekmate/utils/Multicall.vy b/src/snekmate/utils/Multicall.vy index 1292e732..a441ec06 100644 --- a/src/snekmate/utils/Multicall.vy +++ b/src/snekmate/utils/Multicall.vy @@ -26,11 +26,15 @@ """ +# @dev Stores the 1-byte upper bound for the dynamic arrays. +_DYNARRAY_BOUND: constant(uint8) = max_value(uint8) + + # @dev Batch struct for ordinary (i.e. `nonpayable`) function calls. struct Batch: target: address allow_failure: bool - call_data: Bytes[max_value(uint16)] + calldata: Bytes[1_024] # @dev Batch struct for `payable` function calls. @@ -38,14 +42,14 @@ struct BatchValue: target: address allow_failure: bool value: uint256 - call_data: Bytes[max_value(uint16)] + calldata: Bytes[1_024] # @dev Batch struct for ordinary (i.e. `nonpayable`) function calls # using this contract as destination address. struct BatchSelf: allow_failure: bool - call_data: Bytes[max_value(uint16)] + calldata: Bytes[1_024] # @dev Result struct for function call results. @@ -65,8 +69,8 @@ def __init__(): pass -@external -def multicall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result, max_value(uint8)]: +@internal +def _multicall(data: DynArray[Batch, _DYNARRAY_BOUND]) -> DynArray[Result, _DYNARRAY_BOUND]: """ @dev Aggregates function calls, ensuring that each function returns successfully if required. @@ -78,24 +82,24 @@ def multicall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result, max_v @param data The array of `Batch` structs. @return DynArray The array of `Result` structs. """ - results: DynArray[Result, max_value(uint8)] = [] + results: DynArray[Result, _DYNARRAY_BOUND] = [] return_data: Bytes[max_value(uint8)] = b"" success: bool = empty(bool) for batch: Batch in data: if (batch.allow_failure == False): - return_data = raw_call(batch.target, batch.call_data, max_outsize=255) + return_data = raw_call(batch.target, batch.calldata, max_outsize=255) success = True results.append(Result(success=success, return_data=return_data)) else: success, return_data = \ - raw_call(batch.target, batch.call_data, max_outsize=255, revert_on_failure=False) + raw_call(batch.target, batch.calldata, max_outsize=255, revert_on_failure=False) results.append(Result(success=success, return_data=return_data)) return results -@external +@internal @payable -def multicall_value(data: DynArray[BatchValue, max_value(uint8)]) -> DynArray[Result, max_value(uint8)]: +def _multicall_value(data: DynArray[BatchValue, _DYNARRAY_BOUND]) -> DynArray[Result, _DYNARRAY_BOUND]: """ @dev Aggregates function calls with a `msg.value`, ensuring that each function returns successfully @@ -109,7 +113,7 @@ def multicall_value(data: DynArray[BatchValue, max_value(uint8)]) -> DynArray[Re @return DynArray The array of `Result` structs. """ value_accumulator: uint256 = empty(uint256) - results: DynArray[Result, max_value(uint8)] = [] + results: DynArray[Result, _DYNARRAY_BOUND] = [] return_data: Bytes[max_value(uint8)] = b"" success: bool = empty(bool) for batch: BatchValue in data: @@ -121,19 +125,19 @@ def multicall_value(data: DynArray[BatchValue, max_value(uint8)]) -> DynArray[Re # https://twitter.com/Guhu95/status/1736983530343981307. value_accumulator = unsafe_add(value_accumulator, msg_value) if (batch.allow_failure == False): - return_data = raw_call(batch.target, batch.call_data, max_outsize=255, value=msg_value) + return_data = raw_call(batch.target, batch.calldata, max_outsize=255, value=msg_value) success = True results.append(Result(success=success, return_data=return_data)) else: success, return_data = \ - raw_call(batch.target, batch.call_data, max_outsize=255, value=msg_value, revert_on_failure=False) + raw_call(batch.target, batch.calldata, max_outsize=255, value=msg_value, revert_on_failure=False) results.append(Result(success=success, return_data=return_data)) assert msg.value == value_accumulator, "Multicall: value mismatch" return results -@external -def multicall_self(data: DynArray[BatchSelf, max_value(uint8)]) -> DynArray[Result, max_value(uint8)]: +@internal +def _multicall_self(data: DynArray[BatchSelf, _DYNARRAY_BOUND]) -> DynArray[Result, _DYNARRAY_BOUND]: """ @dev Aggregates function calls using `DELEGATECALL`, ensuring that each function returns successfully @@ -152,24 +156,24 @@ def multicall_self(data: DynArray[BatchSelf, max_value(uint8)]) -> DynArray[Resu @param data The array of `BatchSelf` structs. @return DynArray The array of `Result` structs. """ - results: DynArray[Result, max_value(uint8)] = [] + results: DynArray[Result, _DYNARRAY_BOUND] = [] return_data: Bytes[max_value(uint8)] = b"" success: bool = empty(bool) for batch: BatchSelf in data: if (batch.allow_failure == False): - return_data = raw_call(self, batch.call_data, max_outsize=255, is_delegate_call=True) + return_data = raw_call(self, batch.calldata, max_outsize=255, is_delegate_call=True) success = True results.append(Result(success=success, return_data=return_data)) else: success, return_data = \ - raw_call(self, batch.call_data, max_outsize=255, is_delegate_call=True, revert_on_failure=False) + raw_call(self, batch.calldata, max_outsize=255, is_delegate_call=True, revert_on_failure=False) results.append(Result(success=success, return_data=return_data)) return results -@external +@internal @view -def multistaticcall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result, max_value(uint8)]: +def _multistaticcall(data: DynArray[Batch, _DYNARRAY_BOUND]) -> DynArray[Result, _DYNARRAY_BOUND]: """ @dev Aggregates static function calls, ensuring that each function returns successfully if required. @@ -179,16 +183,16 @@ def multistaticcall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result, @param data The array of `Batch` structs. @return DynArray The array of `Result` structs. """ - results: DynArray[Result, max_value(uint8)] = [] + results: DynArray[Result, _DYNARRAY_BOUND] = [] return_data: Bytes[max_value(uint8)] = b"" success: bool = empty(bool) for batch: Batch in data: if (batch.allow_failure == False): - return_data = raw_call(batch.target, batch.call_data, max_outsize=255, is_static_call=True) + return_data = raw_call(batch.target, batch.calldata, max_outsize=255, is_static_call=True) success = True results.append(Result(success=success, return_data=return_data)) else: success, return_data = \ - raw_call(batch.target, batch.call_data, max_outsize=255, is_static_call=True, revert_on_failure=False) + raw_call(batch.target, batch.calldata, max_outsize=255, is_static_call=True, revert_on_failure=False) results.append(Result(success=success, return_data=return_data)) return results diff --git a/src/snekmate/utils/mocks/MulticallMock.vy b/src/snekmate/utils/mocks/MulticallMock.vy new file mode 100644 index 00000000..d4581655 --- /dev/null +++ b/src/snekmate/utils/mocks/MulticallMock.vy @@ -0,0 +1,97 @@ +# pragma version ~=0.4.0b6 +""" +@title Multicall Module Reference Implementation +@custom:contract-name MulticallMock +@license GNU Affero General Public License v3.0 only +@author pcaversaccio +""" + + +# @dev We import the `Multicall` module. +# @notice Please note that the `Multicall` module +# is stateless and therefore does not require +# the `initializes` keyword for initialisation. +from .. import Multicall as mc + + +@deploy +@payable +def __init__(): + """ + @dev To omit the opcodes for checking the `msg.value` + in the creation-time EVM bytecode, the constructor + is declared as `payable`. + """ + pass + + +@external +def multicall(data: DynArray[mc.Batch, mc._DYNARRAY_BOUND]) -> DynArray[mc.Result, mc._DYNARRAY_BOUND]: + """ + @dev Aggregates function calls, ensuring that each + function returns successfully if required. + Since this function uses `CALL`, the `msg.sender` + will be the multicall contract itself. + @notice It is important to note that an external call + via `raw_call` does not perform an external code + size check on the target address. + @param data The array of `Batch` structs. + @return DynArray The array of `Result` structs. + """ + return mc._multicall(data) + + +@external +@payable +def multicall_value(data: DynArray[mc.BatchValue, mc._DYNARRAY_BOUND]) -> DynArray[mc.Result, mc._DYNARRAY_BOUND]: + """ + @dev Aggregates function calls with a `msg.value`, + ensuring that each function returns successfully + if required. Since this function uses `CALL`, + the `msg.sender` will be the multicall contract + itself. + @notice It is important to note that an external call + via `raw_call` does not perform an external code + size check on the target address. + @param data The array of `BatchValue` structs. + @return DynArray The array of `Result` structs. + """ + return mc._multicall_value(data) + + +@external +def multicall_self(data: DynArray[mc.BatchSelf, mc._DYNARRAY_BOUND]) -> DynArray[mc.Result, mc._DYNARRAY_BOUND]: + """ + @dev Aggregates function calls using `DELEGATECALL`, + ensuring that each function returns successfully + if required. Since this function uses `DELEGATECALL`, + the `msg.sender` remains the same account that + invoked the function `multicall_self` in the first place. + @notice Developers can include this function in their own + contract so that users can submit multiple function + calls in one transaction. Since the `msg.sender` is + preserved, it's equivalent to sending multiple transactions + from an EOA (externally-owned account, i.e. non-contract account). + + Furthermore, it is important to note that an external + call via `raw_call` does not perform an external code + size check on the target address. + @param data The array of `BatchSelf` structs. + @return DynArray The array of `Result` structs. + """ + return mc._multicall_self(data) + + +@external +@view +def multistaticcall(data: DynArray[mc.Batch, mc._DYNARRAY_BOUND]) -> DynArray[mc.Result, mc._DYNARRAY_BOUND]: + """ + @dev Aggregates static function calls, ensuring that each + function returns successfully if required. + @notice It is important to note that an external call + via `raw_call` does not perform an external code + size check on the target address. + @param data The array of `Batch` structs. + @return DynArray The array of `Result` structs. + """ + return mc._multistaticcall(data) diff --git a/test/utils/Multicall.t.sol b/test/utils/Multicall.t.sol index 8f8f93db..9bf336cf 100644 --- a/test/utils/Multicall.t.sol +++ b/test/utils/Multicall.t.sol @@ -21,7 +21,10 @@ contract MulticallTest is Test { function setUp() public { multicall = IMulticall( - vyperDeployer.deployContract("src/snekmate/utils/", "Multicall") + vyperDeployer.deployContract( + "src/snekmate/utils/mocks/", + "MulticallMock" + ) ); }