Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ MultiRolesAuthorityTest:testSetPublicCapabilities() (gas: 27762)
MultiRolesAuthorityTest:testSetRoleCapabilities() (gas: 28968)
MultiRolesAuthorityTest:testSetRoles() (gas: 28971)
MultiRolesAuthorityTest:testSetTargetCustomAuthority() (gas: 27976)
MulticallableTest:testMulticallableBenchmark() (gas: 34360)
MulticallableTest:testMulticallableOriginalBenchmark() (gas: 39134)
MulticallableTest:testMulticallablePreservesMsgSender() (gas: 11665)
MulticallableTest:testMulticallablePreservesMsgValue() (gas: 38091)
MulticallableTest:testMulticallablePreservesMsgValueUsedTwice() (gas: 40277)
MulticallableTest:testMulticallableReturnDataIsProperlyEncoded() (gas: 12803)
MulticallableTest:testMulticallableRevertWithCustomError() (gas: 10126)
MulticallableTest:testMulticallableRevertWithMessage() (gas: 11948)
MulticallableTest:testMulticallableWithNoData() (gas: 6496)
OwnedTest:testCallFunctionAsNonOwner() (gas: 11311)
OwnedTest:testCallFunctionAsOwner() (gas: 10479)
OwnedTest:testSetOwner() (gas: 13035)
Expand Down
2 changes: 1 addition & 1 deletion lib/ds-test
Submodule ds-test updated 1 files
+4 −4 src/test.sol
112 changes: 112 additions & 0 deletions src/test/Multicallable.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {DSTestPlus} from "./utils/DSTestPlus.sol";

import {MockMulticallable} from "./utils/mocks/MockMulticallable.sol";

contract MulticallableTest is DSTestPlus {
MockMulticallable multicallable;

function setUp() public {
multicallable = new MockMulticallable();
}

function testMulticallableRevertWithMessage(string memory revertMessage) public {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockMulticallable.revertsWithString.selector, revertMessage);
hevm.expectRevert(bytes(revertMessage));
multicallable.multicall(data);
}

function testMulticallableRevertWithMessage() public {
testMulticallableRevertWithMessage("Milady");
}

function testMulticallableRevertWithCustomError() public {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockMulticallable.revertsWithCustomError.selector);
hevm.expectRevert(MockMulticallable.CustomError.selector);
multicallable.multicall(data);
}

function testMulticallableReturnDataIsProperlyEncoded(
uint256 a0,
uint256 b0,
uint256 a1,
uint256 b1
) public {
bytes[] memory data = new bytes[](2);
data[0] = abi.encodeWithSelector(MockMulticallable.returnsTuple.selector, a0, b0);
data[1] = abi.encodeWithSelector(MockMulticallable.returnsTuple.selector, a1, b1);
bytes[] memory returnedData = multicallable.multicall(data);
MockMulticallable.Tuple memory t0 = abi.decode(returnedData[0], (MockMulticallable.Tuple));
MockMulticallable.Tuple memory t1 = abi.decode(returnedData[1], (MockMulticallable.Tuple));
assertEq(t0.a, a0);
assertEq(t0.b, b0);
assertEq(t1.a, a1);
assertEq(t1.b, b1);
}

function testMulticallableReturnDataIsProperlyEncoded(string memory sIn) public {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockMulticallable.returnsString.selector, sIn);
string memory sOut = abi.decode(multicallable.multicall(data)[0], (string));
assertEq(sIn, sOut);
}

function testMulticallableReturnDataIsProperlyEncoded() public {
testMulticallableReturnDataIsProperlyEncoded(0, 1, 2, 3);
}

function testMulticallableBenchmark() public {
unchecked {
bytes[] memory data = new bytes[](10);
for (uint256 i; i != data.length; ++i) {
data[i] = abi.encodeWithSelector(MockMulticallable.returnsTuple.selector, i, i + 1);
}
bytes[] memory returnedData = multicallable.multicall(data);
assertEq(returnedData.length, data.length);
}
}

function testMulticallableOriginalBenchmark() public {
unchecked {
bytes[] memory data = new bytes[](10);
for (uint256 i; i != data.length; ++i) {
data[i] = abi.encodeWithSelector(MockMulticallable.returnsTuple.selector, i, i + 1);
}
bytes[] memory returnedData = multicallable.multicallOriginal(data);
assertEq(returnedData.length, data.length);
}
}

function testMulticallableWithNoData() public {
bytes[] memory data = new bytes[](0);
assertEq(multicallable.multicall(data).length, 0);
}

function testMulticallablePreservesMsgValue() public {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockMulticallable.pay.selector);
multicallable.multicall{value: 3}(data);
assertEq(multicallable.paid(), 3);
}

function testMulticallablePreservesMsgValueUsedTwice() public {
bytes[] memory data = new bytes[](2);
data[0] = abi.encodeWithSelector(MockMulticallable.pay.selector);
data[1] = abi.encodeWithSelector(MockMulticallable.pay.selector);
multicallable.multicall{value: 3}(data);
assertEq(multicallable.paid(), 6);
}

function testMulticallablePreservesMsgSender() public {
address caller = address(uint160(0xbeef));
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockMulticallable.returnsSender.selector);
hevm.prank(caller);
address returnedAddress = abi.decode(multicallable.multicall(data)[0], (address));
assertEq(caller, returnedAddress);
}
}
61 changes: 61 additions & 0 deletions src/test/utils/mocks/MockMulticallable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import "../../../utils/Multicallable.sol";

contract MockMulticallable is Multicallable {
error CustomError();

struct Tuple {
uint256 a;
uint256 b;
}

function revertsWithString(string memory e) external pure {
revert(e);
}

function revertsWithCustomError() external pure {
revert CustomError();
}

function revertsWithNothing() external pure {
revert();
}

function returnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) {
tuple = Tuple({a: a, b: b});
}

function returnsString(string calldata s) external pure returns (string memory) {
return s;
}

uint256 public paid;

function pay() external payable {
paid += msg.value;
}

function returnsSender() external view returns (address) {
return msg.sender;
}

function multicallOriginal(bytes[] calldata data) public payable returns (bytes[] memory results) {
unchecked {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
}
}
61 changes: 61 additions & 0 deletions src/utils/Multicallable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

/// @notice Contract that enables a single call to call multiple methods on itself.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/Multicallable.sol)
/// @author Modified from Solady (https://github.com/vectorized/solady/blob/main/src/utils/Multicallable.sol)
/// @dev WARNING!
/// Multicallable is NOT SAFE for use in contracts with checks / requires on `msg.value`
/// (e.g. in NFT minting / auction contracts) without a suitable nonce mechanism.
/// It WILL open up your contract to double-spend vulnerabilities / exploits.
/// See: (https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong/)
abstract contract Multicallable {
function multicall(bytes[] calldata data) public payable returns (bytes[] memory results) {
assembly {
if data.length {
results := mload(0x40) // Point `results` to start of free memory.
mstore(results, data.length) // Store `data.length` into `results`.
results := add(results, 0x20)

// `shl` 5 is equivalent to multiplying by 0x20.
let end := shl(5, data.length)
// Copy the offsets from calldata into memory.
calldatacopy(results, data.offset, end)
// Pointer to the top of the memory (i.e. start of the free memory).
let memPtr := add(results, end)
end := add(results, end)

// prettier-ignore
for {} 1 {} {
// The offset of the current bytes in the calldata.
let o := add(data.offset, mload(results))
// Copy the current bytes from calldata to the memory.
calldatacopy(
memPtr,
add(o, 0x20), // The offset of the current bytes' bytes.
calldataload(o) // The length of the current bytes.
)
if iszero(delegatecall(gas(), address(), memPtr, calldataload(o), 0x00, 0x00)) {
// Bubble up the revert if the delegatecall reverts.
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
// Append the current `memPtr` into `results`.
mstore(results, memPtr)
results := add(results, 0x20)
// Append the `returndatasize()`, and the return data.
mstore(memPtr, returndatasize())
returndatacopy(add(memPtr, 0x20), 0x00, returndatasize())
// Advance the `memPtr` by `returndatasize() + 0x20`,
// rounded up to the next multiple of 32.
memPtr := and(add(add(memPtr, returndatasize()), 0x3f), 0xffffffffffffffe0)
// prettier-ignore
if iszero(lt(results, end)) { break }
}
// Restore `results` and allocate memory for it.
results := mload(0x40)
mstore(0x40, memPtr)
}
}
}
}