Skip to content

Commit

Permalink
Fix type hash
Browse files Browse the repository at this point in the history
  • Loading branch information
hiep-immutable committed Aug 22, 2024
1 parent 1de0fd9 commit b6580d3
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 19 deletions.
43 changes: 24 additions & 19 deletions contracts/multicall/GuardedMulticallerV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ contract GuardedMulticallerV2 is AccessControl, ReentrancyGuard, EIP712 {
/// @dev Only those with MULTICALL_SIGNER_ROLE can generate valid signatures for execute function.
bytes32 public constant MULTICALL_SIGNER_ROLE = bytes32("MULTICALL_SIGNER_ROLE");

/// @dev EIP712 typehash for call
bytes32 internal constant CALL_TYPEHASH = keccak256("Call(address target,string functionSignature,bytes data)");

/// @dev EIP712 typehash for execute function
bytes32 internal constant MULTICALL_TYPEHASH =
keccak256("Multicall(bytes32 ref,address[] targets,bytes[] data,uint256 deadline)");
keccak256(
"Multicall(bytes32 ref,Call[] call,uint256 deadline)Call(address target,string functionSignature,bytes data)"
);

/// @dev Event emitted when execute function is called
event Multicalled(address indexed _multicallSigner, bytes32 indexed _reference, Call[] _calls, uint256 _deadline);
Expand Down Expand Up @@ -90,22 +95,6 @@ contract GuardedMulticallerV2 is AccessControl, ReentrancyGuard, EIP712 {
_grantRole(DEFAULT_ADMIN_ROLE, _owner);
}

/**
*
* @dev Returns hash of array of calls
*
* @param _calls Array of calls
*/
function hashCallArray(Call[] calldata _calls) public pure returns (bytes32) {
bytes32[] memory hashedCallArr = new bytes32[](_calls.length);
for (uint256 i = 0; i < _calls.length; i++) {
hashedCallArr[i] = keccak256(
abi.encodePacked(_calls[i].target, _calls[i].functionSignature, _calls[i].data)
);
}
return keccak256(abi.encodePacked(hashedCallArr));
}

/**
*
* @notice Execute a list of calls. Returned data from calls are ignored.
Expand Down Expand Up @@ -217,6 +206,22 @@ contract GuardedMulticallerV2 is AccessControl, ReentrancyGuard, EIP712 {
return replayProtection[_reference];
}

/**
*
* @dev Returns hash of array of calls
*
* @param _calls Array of calls
*/
function _hashCallArray(Call[] calldata _calls) internal pure returns (bytes32) {
bytes32[] memory hashedCallArr = new bytes32[](_calls.length);
for (uint256 i = 0; i < _calls.length; i++) {
hashedCallArr[i] = keccak256(
abi.encode(CALL_TYPEHASH, _calls[i].target, _calls[i].functionSignature, _calls[i].data)
);
}
return keccak256(abi.encodePacked(hashedCallArr));
}

/**
*
* @dev Returns EIP712 message hash for given parameters
Expand All @@ -229,8 +234,8 @@ contract GuardedMulticallerV2 is AccessControl, ReentrancyGuard, EIP712 {
bytes32 _reference,
Call[] calldata _calls,
uint256 _deadline
) internal view returns (bytes32) {
) public view returns (bytes32) {
return
_hashTypedDataV4(keccak256(abi.encode(MULTICALL_TYPEHASH, _reference, hashCallArray(_calls), _deadline)));
_hashTypedDataV4(keccak256(abi.encode(MULTICALL_TYPEHASH, _reference, _hashCallArray(_calls), _deadline)));
}
}
72 changes: 72 additions & 0 deletions test/multicall/GuardedMulticallerV2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

import "forge-std/Test.sol";
import {GuardedMulticallerV2} from "../../contracts/multicall/GuardedMulticallerV2.sol";
import {MockFunctions} from "../../contracts/mocks/MockFunctions.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {SigUtils} from "./SigUtils.t.sol";

contract GuardedMulticallerV2Test is Test {
GuardedMulticallerV2 gmc;
SigUtils sigUtils;
MockFunctions target;

address defaultAdmin = makeAddr("defaultAdmin");
address signer;
uint256 signerPk;

function setUp() public {
target = new MockFunctions();
(signer, signerPk) = makeAddrAndKey("signer");

gmc = new GuardedMulticallerV2(defaultAdmin, "name", "1");
vm.prank(defaultAdmin);
gmc.grantMulticallSignerRole(signer);

sigUtils = new SigUtils("name", "1", address(gmc));
}

function test_Roles() public {
assertTrue(gmc.hasRole(gmc.DEFAULT_ADMIN_ROLE(), defaultAdmin));
assertTrue(gmc.hasRole(gmc.MULTICALL_SIGNER_ROLE(), signer));
}

function test_Execute() public {
bytes32 ref = keccak256("ref");
uint256 deadline = block.timestamp + 1;
GuardedMulticallerV2.Call[] memory calls = new GuardedMulticallerV2.Call[](1);
calls[0] = GuardedMulticallerV2.Call(address(target), "succeed()", "");
bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest);
bytes memory signature = abi.encodePacked(r, s, v);
gmc.execute(signer, ref, calls, deadline, signature);
assertTrue(gmc.hasBeenExecuted(ref));
}

function test_RevertWhen_UnauthorizedSigner() public {
(address fakeSigner, uint256 fakeSignerPk) = makeAddrAndKey("fakeSigner");
bytes32 ref = keccak256("ref");
uint256 deadline = block.timestamp + 1;
GuardedMulticallerV2.Call[] memory calls = new GuardedMulticallerV2.Call[](1);
calls[0] = GuardedMulticallerV2.Call(address(target), "revertWithNoReason()", "");
bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(fakeSignerPk, digest);
bytes memory signature = abi.encodePacked(r, s, v);
vm.expectRevert(abi.encodeWithSelector(GuardedMulticallerV2.UnauthorizedSigner.selector, fakeSigner));
gmc.execute(fakeSigner, ref, calls, deadline, signature);
}

function test_RevertWhen_CallFailed() public {
bytes32 ref = keccak256("ref");
uint256 deadline = block.timestamp + 1;
GuardedMulticallerV2.Call[] memory calls = new GuardedMulticallerV2.Call[](1);
calls[0] = GuardedMulticallerV2.Call(address(target), "revertWithNoReason()", "");
bytes32 digest = sigUtils.hashTypedData(ref, calls, deadline);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest);
bytes memory signature = abi.encodePacked(r, s, v);
vm.expectRevert(abi.encodeWithSelector(GuardedMulticallerV2.FailedCall.selector, calls[0]));
gmc.execute(signer, ref, calls, deadline, signature);
}
}
41 changes: 41 additions & 0 deletions test/multicall/SigUtils.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {GuardedMulticallerV2} from "../../contracts/multicall/GuardedMulticallerV2.sol";

contract SigUtils {
bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

bytes32 internal constant CALL_TYPEHASH = keccak256("Call(address target,string functionSignature,bytes data)");

bytes32 internal constant MULTICALL_TYPEHASH =
keccak256(
"Multicall(bytes32 ref,Call[] call,uint256 deadline)Call(address target,string functionSignature,bytes data)"
);

bytes32 private immutable cachedDomainSeparator;

constructor(string memory _name, string memory _version, address _verifyingContract) {
cachedDomainSeparator = keccak256(abi.encode(_TYPE_HASH, keccak256(bytes(_name)), keccak256(bytes(_version)), block.chainid, _verifyingContract));
}

function _hashCallArray(GuardedMulticallerV2.Call[] calldata _calls) internal pure returns (bytes32) {
bytes32[] memory hashedCallArr = new bytes32[](_calls.length);
for (uint256 i = 0; i < _calls.length; i++) {
hashedCallArr[i] = keccak256(
abi.encode(CALL_TYPEHASH, _calls[i].target, _calls[i].functionSignature, _calls[i].data)
);
}
return keccak256(abi.encodePacked(hashedCallArr));
}

function hashTypedData(
bytes32 _reference,
GuardedMulticallerV2.Call[] calldata _calls,
uint256 _deadline
) public view returns (bytes32) {
bytes32 digest = keccak256(abi.encode(MULTICALL_TYPEHASH, _reference, _hashCallArray(_calls), _deadline));
return keccak256(abi.encodePacked("\x19\x01", cachedDomainSeparator, digest));
}
}

0 comments on commit b6580d3

Please sign in to comment.