diff --git a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol new file mode 100644 index 0000000000..1f48e59609 --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Interface for a contract that can be called multiple times by the same caller. Inspired by MulticallV3: +/// https://github.com/mds1/multicall/blob/master/src/Multicall3.sol +interface IMulticallTarget { + struct Result { + bool success; + bytes returnData; + } + + function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external; + function multicallWithResults( + bytes[] calldata data, + bool ignoreReverts + ) + external + returns (Result[] memory results); +} diff --git a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol new file mode 100644 index 0000000000..bed9266c33 --- /dev/null +++ b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IMulticallTarget} from "../interfaces/IMulticallTarget.sol"; + +// solhint-disable avoid-low-level-calls +/// @notice Template for a contract that supports batched calls (preserving the msg.sender). +/// Only calls with zero msg.value could be batched. +abstract contract MulticallTarget is IMulticallTarget { + error MulticallTarget__UndeterminedRevert(); + + /// @notice Perform a batched call to this contract, preserving the msg.sender. + /// The return data from each call is discarded. + /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. + /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. + /// Otherwise, the whole batch call will be reverted with the original revert reason. + /// @param data List of abi-encoded calldata for the calls to perform. + /// @param ignoreReverts Whether to ignore the revert errors from the calls. + function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external { + for (uint256 i = 0; i < data.length; ++i) { + // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` + // calling the functions directly one by one, therefore doesn't add any security risks. + // Note: msg.value is also preserved when doing a delegate call, but this function is not payable, + // so it's always 0 and not a security risk. + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + if (!success && !ignoreReverts) { + _bubbleRevert(result); + } + } + } + + /// @notice Perform a batched call to this contract, preserving the msg.sender. + /// The return data from each call is preserved. + /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. + /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. + /// Otherwise, the whole batch call will be reverted with the original revert reason. + /// @param data List of abi-encoded calldata for the calls to perform. + /// @param ignoreReverts Whether to ignore the revert errors from the calls. + /// @return results List of results from the calls: `(success, returnData)`. + function multicallWithResults( + bytes[] calldata data, + bool ignoreReverts + ) + external + returns (Result[] memory results) + { + results = new Result[](data.length); + for (uint256 i = 0; i < data.length; ++i) { + // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` + // calling the functions directly one by one, therefore doesn't add any security risks. + // Note: msg.value is also preserved when doing a delegate call, but this function is not payable, + // so it's always 0 and not a security risk. + (results[i].success, results[i].returnData) = address(this).delegatecall(data[i]); + if (!results[i].success && !ignoreReverts) { + _bubbleRevert(results[i].returnData); + } + } + } + + /// @dev Bubbles the revert message from the underlying call. + /// Note: preserves the same custom error or revert string, if one was used. + /// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/Address.sol#L143-L158 + function _bubbleRevert(bytes memory returnData) internal pure { + // Look for revert reason and bubble it up if present + if (returnData.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert MulticallTarget__UndeterminedRevert(); + } + } +} diff --git a/packages/contracts-rfq/test/MulticallTarget.t.sol b/packages/contracts-rfq/test/MulticallTarget.t.sol new file mode 100644 index 0000000000..1c12bcaca5 --- /dev/null +++ b/packages/contracts-rfq/test/MulticallTarget.t.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IMulticallTarget} from "../contracts/interfaces/IMulticallTarget.sol"; +import {MulticallTargetHarness, MulticallTarget} from "./harnesses/MulticallTargetHarness.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract MulticallTargetTest is Test { + MulticallTargetHarness public harness; + + address public caller = makeAddr("Caller"); + + function setUp() public { + harness = new MulticallTargetHarness(); + harness.setAddressField(address(1)); + harness.setUintField(2); + } + + function getEncodedStringRevertMessage() internal view returns (bytes memory) { + return abi.encodeWithSignature("Error(string)", harness.REVERT_MESSAGE()); + } + + function getMsgSenderData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.addressField, ()), + abi.encodeCall(harness.setMsgSenderAsAddressField, ()), + abi.encodeCall(harness.addressField, ()) + ); + } + + function getMsgSenderResults() internal view returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(caller)), + IMulticallTarget.Result(true, abi.encode(caller)) + ); + } + + function getNoRevertsData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.addressField, ()), + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getNoRevertsResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1))), + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getCustomErrorRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.customErrorRevert, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getCustomErrorRevertResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, abi.encodeWithSelector(MulticallTargetHarness.CustomError.selector)), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getStringRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.revertingFunction, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getStringRevertResults() internal view returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, abi.encodeWithSignature("Error(string)", harness.REVERT_MESSAGE())), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getUndeterminedRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.undeterminedRevert, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getUndeterminedRevertResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, ""), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + // ══════════════════════════════════════════ MULTICALL (NO RESULTS) ═══════════════════════════════════════════════ + + function test_multicallNoResults_ignoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallNoResults_ignoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_dontIgnoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + harness.multicallNoResults({data: data, ignoreReverts: false}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_dontIgnoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + harness.multicallNoResults({data: data, ignoreReverts: false}); + + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallNoResults_dontIgnoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + vm.expectRevert(MulticallTargetHarness.CustomError.selector); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + function test_multicallNoResults_dontIgnoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + string memory revertMessage = harness.REVERT_MESSAGE(); + vm.expectRevert(bytes(revertMessage)); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + function test_multicallNoResults_dontIgnoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + vm.expectRevert(MulticallTarget.MulticallTarget__UndeterminedRevert.selector); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + // ═════════════════════════════════════════ MULTICALL (WITH RESULTS) ══════════════════════════════════════════════ + + function test_multicallWithResults_ignoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getNoRevertsResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getMsgSenderResults()); + assertEq(harness.uintField(), 2); + assertEq(harness.addressField(), caller); + } + + function test_multicallWithResults_ignoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getCustomErrorRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getStringRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getUndeterminedRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_dontIgnoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: false}); + + assertEq(results, getNoRevertsResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_dontIgnoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: false}); + + assertEq(results, getMsgSenderResults()); + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallWithResults_dontIgnoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + vm.expectRevert(MulticallTargetHarness.CustomError.selector); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + function test_multicallWithResults_dontIgnoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + string memory revertMessage = harness.REVERT_MESSAGE(); + vm.expectRevert(bytes(revertMessage)); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + function test_multicallWithResults_dontIgnoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + vm.expectRevert(MulticallTarget.MulticallTarget__UndeterminedRevert.selector); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + // ══════════════════════════════════════════════════ VIEW ════════════════════════════════════════════════════ + + function assertEq(IMulticallTarget.Result memory a, IMulticallTarget.Result memory b) internal pure { + assertEq(a.success, b.success); + assertEq(a.returnData, b.returnData); + } + + function assertEq(IMulticallTarget.Result[] memory a, IMulticallTarget.Result[] memory b) internal pure { + assertEq(a.length, b.length); + for (uint256 i = 0; i < a.length; i++) { + assertEq(a[i], b[i]); + } + } + + function toArray( + bytes memory a, + bytes memory b, + bytes memory c, + bytes memory d + ) + internal + pure + returns (bytes[] memory arr) + { + arr = new bytes[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + } + + function toArray( + IMulticallTarget.Result memory a, + IMulticallTarget.Result memory b, + IMulticallTarget.Result memory c, + IMulticallTarget.Result memory d + ) + internal + pure + returns (IMulticallTarget.Result[] memory arr) + { + arr = new IMulticallTarget.Result[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + } +} diff --git a/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol new file mode 100644 index 0000000000..5819dbf3fc --- /dev/null +++ b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MulticallTarget} from "../../contracts/utils/MulticallTarget.sol"; + +contract MulticallTargetHarness is MulticallTarget { + address public addressField; + uint256 public uintField; + + string public constant REVERT_MESSAGE = "gm, this is a revert message"; + + error CustomError(); + + function setMsgSenderAsAddressField() external returns (address) { + addressField = msg.sender; + return addressField; + } + + function setAddressField(address _addressField) external returns (address) { + addressField = _addressField; + return addressField; + } + + function setUintField(uint256 _uintField) external returns (uint256) { + uintField = _uintField; + return uintField; + } + + function customErrorRevert() external pure { + revert CustomError(); + } + + function revertingFunction() external pure { + revert(REVERT_MESSAGE); + } + + function undeterminedRevert() external pure { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(0, 0) + } + } +}