Skip to content

Commit

Permalink
♻️ Make Multicall Module-Friendly (#232)
Browse files Browse the repository at this point in the history
### 🕓 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 <pascal.caversaccio@hotmail.ch>
  • Loading branch information
pcaversaccio authored Apr 10, 2024
1 parent d5a9c8d commit a0c1145
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 34 deletions.
18 changes: 9 additions & 9 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 27 additions & 23 deletions src/snekmate/utils/Multicall.vy
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,30 @@
"""


# @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.
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.
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
97 changes: 97 additions & 0 deletions src/snekmate/utils/mocks/MulticallMock.vy
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 4 additions & 1 deletion test/utils/Multicall.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
);
}

Expand Down

0 comments on commit a0c1145

Please sign in to comment.