diff --git a/src/interfaces/ILlamaCore.sol b/src/interfaces/ILlamaCore.sol index d0eba15..c83dda5 100644 --- a/src/interfaces/ILlamaCore.sol +++ b/src/interfaces/ILlamaCore.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.23; import {ILlamaPolicy} from "src/interfaces/ILlamaPolicy.sol"; import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {ActionState} from "src/lib/Enums.sol"; import { Action, ActionInfo, @@ -20,8 +21,14 @@ import { /// @author Llama (devsdosomething@llama.xyz) /// @notice This is the interface for LlamaCore. interface ILlamaCore { + error InvalidSignature(); + error PolicyholderDoesNotHavePermission(); + /// @dev The action is not in the expected state. + /// @param current The current state of the action. + error InvalidActionState(ActionState current); + function actionGuard(address target, bytes4 selector) external view returns (address guard); function actionsCount() external view returns (uint256); diff --git a/test/PeripheryTestSetup.sol b/test/PeripheryTestSetup.sol index fa6bdf1..723c374 100644 --- a/test/PeripheryTestSetup.sol +++ b/test/PeripheryTestSetup.sol @@ -35,6 +35,9 @@ contract PeripheryTestSetup is Test { address coreTeam4 = 0x6b45E38c87bfCa15ee90AAe2AFe3CFC58cE08F75; address coreTeam5 = 0xbdfcE43E5D2C7AA8599290d940c9932B8dBC94Ca; + address tokenHolder; + uint256 tokenHolderPrivateKey; + // Function selectors used in tests. bytes4 public constant SET_ROLE_HOLDER_SELECTOR = 0x2524842c; // pause(bool) @@ -51,5 +54,6 @@ contract PeripheryTestSetup is Test { function setUp() public virtual { vm.createSelectFork(MAINNET_RPC_URL, 18_707_845); + (tokenHolder, tokenHolderPrivateKey) = makeAddrAndKey("tokenHolder"); } } diff --git a/test/token-voting/ERC20TokenholderActionCreator.t.sol b/test/token-voting/ERC20TokenholderActionCreator.t.sol index 4191ddc..afca43f 100644 --- a/test/token-voting/ERC20TokenholderActionCreator.t.sol +++ b/test/token-voting/ERC20TokenholderActionCreator.t.sol @@ -4,17 +4,20 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; import {ERC20Votes} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; -import {MockERC20Votes} from "test/mock/MockERC20Votes.sol"; -import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; -import {Action, ActionInfo} from "src/lib/Structs.sol"; -import {RoleDescription} from "src/lib/UDVTs.sol"; import {ILlamaCore} from "src/interfaces/ILlamaCore.sol"; import {ILlamaPolicy} from "src/interfaces/ILlamaPolicy.sol"; import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {ActionState} from "src/lib/Enums.sol"; +import {Action, ActionInfo} from "src/lib/Structs.sol"; +import {RoleDescription} from "src/lib/UDVTs.sol"; import {ERC20TokenholderActionCreator} from "src/token-voting/ERC20TokenholderActionCreator.sol"; import {TokenholderActionCreator} from "src/token-voting/TokenholderActionCreator.sol"; +import {MockERC20Votes} from "test/mock/MockERC20Votes.sol"; +import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; +import {LlamaCoreSigUtils} from "test/utils/LlamaCoreSigUtils.sol"; + contract ERC20TokenholderActionCreatorTest is PeripheryTestSetup { event ActionCreated( uint256 id, @@ -57,7 +60,7 @@ contract Constructor is ERC20TokenholderActionCreatorTest { function test_RevertsIf_InvalidTokenAddress() public { vm.expectRevert(); // will EvmError: Revert vecause totalSupply fn does not exist - new ERC20TokenholderActionCreator(ERC20Votes(makeAddr("invalid-token")), ILlamaCore(address(CORE)), uint256(0)); + new ERC20TokenholderActionCreator(ERC20Votes(makeAddr("invalid-token")), CORE, uint256(0)); } function test_RevertsIf_CreationThresholdExceedsTotalSupply() public { @@ -67,7 +70,7 @@ contract Constructor is ERC20TokenholderActionCreatorTest { vm.warp(block.timestamp + 1); vm.expectRevert(TokenholderActionCreator.InvalidCreationThreshold.selector); - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), 17_000_000_000_000_000_000_000_000); + new ERC20TokenholderActionCreator(token, CORE, 17_000_000_000_000_000_000_000_000); } function test_ProperlySetsConstructorArguments() public { @@ -77,8 +80,7 @@ contract Constructor is ERC20TokenholderActionCreatorTest { vm.warp(block.timestamp + 1); - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), threshold); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, threshold); assertEq(address(actionCreator.TOKEN()), address(token)); assertEq(address(actionCreator.LLAMA_CORE()), address(CORE)); assertEq(actionCreator.creationThreshold(), threshold); @@ -97,8 +99,7 @@ contract TokenHolderCreateAction is ERC20TokenholderActionCreatorTest { vm.warp(block.timestamp + 1); - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), threshold); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, threshold); vm.prank(notATokenHolder); vm.expectRevert(abi.encodeWithSelector(TokenholderActionCreator.InsufficientBalance.selector, 0)); @@ -114,8 +115,7 @@ contract TokenHolderCreateAction is ERC20TokenholderActionCreatorTest { vm.warp(block.timestamp + 1); - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), threshold); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, threshold); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); @@ -136,8 +136,7 @@ contract TokenHolderCreateAction is ERC20TokenholderActionCreatorTest { vm.roll(block.number + 1); vm.warp(block.timestamp + 1); - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), threshold); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, threshold); vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token // voting action creator @@ -195,7 +194,7 @@ contract CancelAction is ERC20TokenholderActionCreatorTest { vm.roll(block.number + 1); vm.warp(block.timestamp + 1); - actionCreator = new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), threshold); + actionCreator = new ERC20TokenholderActionCreator(token, CORE, threshold); vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token // voting action creator @@ -240,8 +239,7 @@ contract SetActionThreshold is ERC20TokenholderActionCreatorTest { vm.warp(block.timestamp + 1); - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), 1_000_000e18); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, 1_000_000e18); assertEq(actionCreator.creationThreshold(), 1_000_000e18); @@ -256,8 +254,7 @@ contract SetActionThreshold is ERC20TokenholderActionCreatorTest { mockErc20Votes.mint(address(this), 500_000e18); // we use mockErc20Votes because IVotesToken is an interface // without the `mint` function - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), 500_000e18); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, 500_000e18); vm.expectRevert(TokenholderActionCreator.InvalidCreationThreshold.selector); vm.prank(address(EXECUTOR)); @@ -270,11 +267,319 @@ contract SetActionThreshold is ERC20TokenholderActionCreatorTest { mockErc20Votes.mint(address(this), 1_000_000e18); // we use mockErc20Votes because IVotesToken is an interface // without the `mint` function - ERC20TokenholderActionCreator actionCreator = - new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), 1_000_000e18); + ERC20TokenholderActionCreator actionCreator = new ERC20TokenholderActionCreator(token, CORE, 1_000_000e18); vm.expectRevert(TokenholderActionCreator.OnlyLlamaExecutor.selector); vm.prank(notLlamaExecutor); actionCreator.setActionThreshold(threshold); } } + +contract CreateActionBySig is ERC20TokenholderActionCreatorTest, LlamaCoreSigUtils { + ERC20TokenholderActionCreator actionCreator; + uint8 actionCreatorRole; + + function setUp() public virtual override { + ERC20TokenholderActionCreatorTest.setUp(); + mockErc20Votes.mint(tokenHolder, 1000); // we use mockErc20Votes because IVotesToken is an + // interface without the `delegate` function + vm.prank(tokenHolder); + mockErc20Votes.delegate(tokenHolder); // we use mockErc20Votes because IVotesToken is an interface without + // the `delegate` function + + actionCreator = new ERC20TokenholderActionCreator(token, CORE, 1000); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(actionCreator) + }) + ); + + vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token + // voting action creator + POLICY.initializeRole(RoleDescription.wrap("Token Voting Action Creator Role")); + actionCreatorRole = 2; + POLICY.setRoleHolder(actionCreatorRole, address(actionCreator), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + POLICY.setRolePermission( + actionCreatorRole, + ILlamaPolicy.PermissionData(address(POLICY), POLICY.initializeRole.selector, address(STRATEGY)), + true + ); + vm.stopPrank(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + } + + function createOffchainSignature(uint256 privateKey) internal view returns (uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = createOffchainSignatureWithDescription(privateKey, ""); + } + + function createOffchainSignatureWithDescription(uint256 privateKey, string memory description) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CreateActionBySig memory _createAction = LlamaCoreSigUtils.CreateActionBySig({ + role: actionCreatorRole, + strategy: address(STRATEGY), + target: address(POLICY), + value: 0, + data: abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + description: description, + tokenHolder: tokenHolder, + nonce: 0 + }); + bytes32 digest = getCreateActionBySigTypedDataHash(_createAction); + (v, r, s) = vm.sign(privateKey, digest); + } + + function createActionBySig(uint8 v, bytes32 r, bytes32 s) internal returns (uint256 actionId) { + actionId = actionCreator.createActionBySig( + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + "", + v, + r, + s + ); + } + + function test_CreatesActionBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + bytes memory data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + + uint256 actionCount = CORE.actionsCount(); + + vm.expectEmit(); + emit ActionCreated(actionCount, tokenHolder, actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + + uint256 actionId = createActionBySig(v, r, s); + Action memory action = CORE.getAction(actionId); + + assertEq(actionId, actionCount); + assertEq(CORE.actionsCount() - 1, actionCount); + assertEq(action.creationTime, block.timestamp); + } + + function test_CreatesActionBySigWithDescription() public { + (uint8 v, bytes32 r, bytes32 s) = + createOffchainSignatureWithDescription(tokenHolderPrivateKey, "# Action 0 \n This is my action."); + bytes memory data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + + uint256 actionCount = CORE.actionsCount(); + + vm.expectEmit(); + emit ActionCreated( + actionCount, + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + data, + "# Action 0 \n This is my action." + ); + + uint256 actionId = actionCreator.createActionBySig( + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + "# Action 0 \n This is my action.", + v, + r, + s + ); + Action memory action = CORE.getAction(actionId); + + assertEq(actionId, actionCount); + assertEq(CORE.actionsCount() - 1, actionCount); + assertEq(action.creationTime, block.timestamp); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.createActionBySig.selector), 0); + createActionBySig(v, r, s); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.createActionBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + createActionBySig(v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as + // policyholder since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } + + function test_RevertIf_SignerIsNotPolicyHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the policyholder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig((v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + + vm.prank(tokenHolder); + actionCreator.incrementNonce(ILlamaCore.createActionBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } +} + +contract CancelActionBySig is ERC20TokenholderActionCreatorTest, LlamaCoreSigUtils { + bytes data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + uint256 actionId; + ERC20TokenholderActionCreator actionCreator; + ActionInfo actionInfo; + uint8 actionCreatorRole; + + function setUp() public virtual override { + ERC20TokenholderActionCreatorTest.setUp(); + mockErc20Votes.mint(address(tokenHolder), 1000); // we use mockErc20Votes because IVotesToken is an + // interface without the `delegate` function + vm.prank(tokenHolder); + mockErc20Votes.delegate(tokenHolder); // we use mockErc20Votes because IVotesToken is an interface without + // the `delegate` function + + actionCreator = new ERC20TokenholderActionCreator(token, ILlamaCore(address(CORE)), 1000); + + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(actionCreator) + }) + ); + + vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token + // voting action creator + POLICY.initializeRole(RoleDescription.wrap("Token Voting Action Creator Role")); + actionCreatorRole = 2; + POLICY.setRoleHolder(actionCreatorRole, address(actionCreator), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + POLICY.setRolePermission( + actionCreatorRole, + ILlamaPolicy.PermissionData(address(POLICY), POLICY.initializeRole.selector, address(STRATEGY)), + true + ); + vm.stopPrank(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + + vm.expectEmit(); + emit ActionCreated(CORE.actionsCount(), tokenHolder, actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + vm.prank(tokenHolder); + actionId = actionCreator.createAction(actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + + actionInfo = ActionInfo(actionId, address(actionCreator), actionCreatorRole, STRATEGY, address(POLICY), 0, data); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CancelActionBySig memory cancelAction = LlamaCoreSigUtils.CancelActionBySig({ + tokenHolder: tokenHolder, + actionInfo: _actionInfo, + nonce: actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector) + }); + bytes32 digest = getCancelActionBySigTypedDataHash(cancelAction); + (v, r, s) = vm.sign(privateKey, digest); + } + + function cancelActionBySig(ActionInfo memory _actionInfo, uint8 v, bytes32 r, bytes32 s) internal { + actionCreator.cancelActionBySig(tokenHolder, _actionInfo, v, r, s); + } + + function test_CancelActionBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + // vm.expectEmit(); + // emit ActionCanceled(actionInfo.id, tokenHolder); + + cancelActionBySig(actionInfo, v, r, s); + + uint256 state = uint256(CORE.getActionState(actionInfo)); + uint256 canceled = uint256(ActionState.Canceled); + assertEq(state, canceled); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector), 0); + cancelActionBySig(actionInfo, v, r, s); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + cancelActionBySig(actionInfo, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsNotTokenHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the policyholder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, (v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + vm.prank(tokenHolder); + actionCreator.incrementNonce(ILlamaCore.cancelActionBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } +} diff --git a/test/token-voting/ERC20TokenholderCaster.t.sol b/test/token-voting/ERC20TokenholderCaster.t.sol index 58dbccf..94316de 100644 --- a/test/token-voting/ERC20TokenholderCaster.t.sol +++ b/test/token-voting/ERC20TokenholderCaster.t.sol @@ -5,6 +5,7 @@ import {Test, console2} from "forge-std/Test.sol"; import {MockERC20Votes} from "test/mock/MockERC20Votes.sol"; +import {ActionState} from "src/lib/Enums.sol"; import {Action, ActionInfo, PermissionData} from "src/lib/Structs.sol"; import {ILlamaCore} from "src/interfaces/ILlamaCore.sol"; import {ILlamaPolicy} from "src/interfaces/ILlamaPolicy.sol"; @@ -15,6 +16,7 @@ import {ERC20Votes} from "lib/openzeppelin-contracts/contracts/token/ERC20/exten import {ERC20TokenholderCaster} from "src/token-voting/ERC20TokenholderCaster.sol"; import {TokenholderCaster} from "src/token-voting/TokenholderCaster.sol"; import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; +import {LlamaCoreSigUtils} from "test/utils/LlamaCoreSigUtils.sol"; contract ERC20TokenholderCasterTest is PeripheryTestSetup { uint256 constant DEFAULT_APPROVAL_THRESHOLD = 1000; @@ -33,7 +35,8 @@ contract ERC20TokenholderCasterTest is PeripheryTestSetup { ILlamaStrategy tokenVotingStrategy; - address tokenHolder1 = makeAddr("tokenholder-1"); + address tokenHolder1; + uint256 tokenHolder1PrivateKey; address tokenHolder2 = makeAddr("tokenholder-2"); address tokenHolder3 = makeAddr("tokenholder-3"); @@ -108,6 +111,9 @@ contract ERC20TokenholderCasterTest is PeripheryTestSetup { function setUp() public virtual override { PeripheryTestSetup.setUp(); + + (tokenHolder1, tokenHolder1PrivateKey) = makeAddrAndKey("tokenholder-1"); + vm.deal(address(this), 1 ether); vm.deal(address(msg.sender), 1 ether); vm.deal(address(EXECUTOR), 1 ether); @@ -121,7 +127,7 @@ contract ERC20TokenholderCasterTest is PeripheryTestSetup { vm.prank(address(EXECUTOR)); POLICY.initializeRole(RoleDescription.wrap("Token Voting Caster Role")); // initializes role 2 vm.prank(address(EXECUTOR)); - POLICY.initializeRole(RoleDescription.wrap("Made Up Role")); // initializes role 2 + POLICY.initializeRole(RoleDescription.wrap("Made Up Role")); // initializes role 3 mockErc20Votes.mint(tokenHolder1, DEFAULT_APPROVAL_THRESHOLD / 2); mockErc20Votes.mint(tokenHolder2, DEFAULT_APPROVAL_THRESHOLD / 2); @@ -540,3 +546,226 @@ contract SubmitDisapprovals is ERC20TokenholderCasterTest { caster.submitDisapprovals(actionInfo); } } + +contract CastApprovalBySig is ERC20TokenholderCasterTest, LlamaCoreSigUtils { + function setUp() public virtual override { + ERC20TokenholderCasterTest.setUp(); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(caster) + }) + ); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CastApprovalBySig memory castApproval = LlamaCoreSigUtils.CastApprovalBySig({ + actionInfo: _actionInfo, + support: 1, + reason: "", + tokenHolder: tokenHolder1, + nonce: 0 + }); + bytes32 digest = getCastApprovalBySigTypedDataHash(castApproval); + (v, r, s) = vm.sign(privateKey, digest); + } + + function castApprovalBySig(ActionInfo memory _actionInfo, uint8 support, uint8 v, bytes32 r, bytes32 s) internal { + caster.castApprovalBySig(tokenHolder1, support, _actionInfo, "", v, r, s); + } + + function test_CastsApprovalBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.expectEmit(); + emit ApprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, block.timestamp - 1), "" + ); + + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + assertEq(caster.nonces(tokenHolder1, TokenholderCaster.castApprovalBySig.selector), 0); + castApprovalBySig(actionInfo, 1, v, r, s); + assertEq(caster.nonces(tokenHolder1, TokenholderCaster.castApprovalBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + castApprovalBySig(actionInfo, 1, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(TokenholderCaster.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_RevertIf_SignerIsNotTokenHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the token holder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, (v + 1), r, s); + } + + function test_RevertIf_TokenHolderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.prank(tokenHolder1); + caster.incrementNonce(ILlamaCore.castApprovalBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } +} + +contract CastDisapprovalBySig is ERC20TokenholderCasterTest, LlamaCoreSigUtils { + function setUp() public virtual override { + ERC20TokenholderCasterTest.setUp(); + + castApprovalsFor(); + + vm.warp(block.timestamp + (1 days * TWO_THIRDS_IN_BPS) / ONE_HUNDRED_IN_BPS); + + vm.prank(tokenHolder1); + caster.submitApprovals(actionInfo); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(caster) + }) + ); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CastDisapprovalBySig memory castDisapproval = LlamaCoreSigUtils.CastDisapprovalBySig({ + actionInfo: _actionInfo, + support: 1, + reason: "", + tokenHolder: tokenHolder1, + nonce: 0 + }); + bytes32 digest = getCastDisapprovalBySigTypedDataHash(castDisapproval); + (v, r, s) = vm.sign(privateKey, digest); + } + + function castDisapprovalBySig(ActionInfo memory _actionInfo, uint8 v, bytes32 r, bytes32 s) internal { + caster.castDisapprovalBySig(tokenHolder1, 1, _actionInfo, "", v, r, s); + } + + function test_CastsDisapprovalBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.expectEmit(); + emit DisapprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, token.clock() - 1), "" + ); + + castDisapprovalBySig(actionInfo, v, r, s); + + // assertEq(CORE.getAction(0).totalDisapprovals, 1); + // assertEq(CORE.disapprovals(0, disapproverDrake), true); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + assertEq(caster.nonces(tokenHolder1, ILlamaCore.castDisapprovalBySig.selector), 0); + castDisapprovalBySig(actionInfo, v, r, s); + assertEq(caster.nonces(tokenHolder1, ILlamaCore.castDisapprovalBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + castDisapprovalBySig(actionInfo, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsNotPolicyHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, (v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.prank(tokenHolder1); + caster.incrementNonce(ILlamaCore.castDisapprovalBySig.selector); + + // Invalid Signature error since the recovered signer address during the second call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_FailsIfDisapproved() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + // First disapproval. + vm.expectEmit(); + emit DisapprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, token.clock() - 1), "" + ); + castDisapprovalBySig(actionInfo, v, r, s); + // assertEq(CORE.getAction(actionInfo.id).totalDisapprovals, 1); + + // Second disapproval. + vm.prank(tokenHolder2); + caster.castDisapproval(actionInfo, 1, ""); + + vm.warp(block.timestamp + 1 + (1 days * TWO_THIRDS_IN_BPS) / ONE_HUNDRED_IN_BPS); + + caster.submitDisapprovals(actionInfo); + + // Assertions. + ActionState state = ActionState(CORE.getActionState(actionInfo)); + assertEq(uint8(state), uint8(ActionState.Failed)); + + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Failed)); + CORE.executeAction(actionInfo); + } +} diff --git a/test/token-voting/ERC721TokenholderActionCreator.t.sol b/test/token-voting/ERC721TokenholderActionCreator.t.sol index 0be6113..f41f576 100644 --- a/test/token-voting/ERC721TokenholderActionCreator.t.sol +++ b/test/token-voting/ERC721TokenholderActionCreator.t.sol @@ -4,9 +4,8 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; import {ERC721Votes} from "lib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Votes.sol"; -import {MockERC721Votes} from "test/mock/MockERC721Votes.sol"; -import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; +import {ActionState} from "src/lib/Enums.sol"; import {Action, ActionInfo} from "src/lib/Structs.sol"; import {RoleDescription} from "src/lib/UDVTs.sol"; import {ILlamaCore} from "src/interfaces/ILlamaCore.sol"; @@ -15,6 +14,10 @@ import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; import {ERC721TokenholderActionCreator} from "src/token-voting/ERC721TokenholderActionCreator.sol"; import {TokenholderActionCreator} from "src/token-voting/TokenholderActionCreator.sol"; +import {MockERC721Votes} from "test/mock/MockERC721Votes.sol"; +import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; +import {LlamaCoreSigUtils} from "test/utils/LlamaCoreSigUtils.sol"; + contract ERC721TokenholderActionCreatorTest is PeripheryTestSetup { event ActionCreated( uint256 id, @@ -283,3 +286,318 @@ contract SetActionThreshold is ERC721TokenholderActionCreatorTest { actionCreator.setActionThreshold(threshold); } } + +contract CreateActionBySig is ERC721TokenholderActionCreatorTest, LlamaCoreSigUtils { + ERC721TokenholderActionCreator actionCreator; + uint8 actionCreatorRole; + + function setUp() public virtual override { + ERC721TokenholderActionCreatorTest.setUp(); + mockErc721Votes.mint(tokenHolder, 0); // we use mockErc721Votes because IVotesToken is an + // interface without the `delegate` function + vm.prank(tokenHolder); + mockErc721Votes.delegate(tokenHolder); // we use mockErc721Votes because IVotesToken is an interface without + // the `delegate` function + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + + actionCreator = new ERC721TokenholderActionCreator(token, CORE, 1); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(actionCreator) + }) + ); + + vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token + // voting action creator + POLICY.initializeRole(RoleDescription.wrap("Token Voting Action Creator Role")); + actionCreatorRole = 2; + POLICY.setRoleHolder(actionCreatorRole, address(actionCreator), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + POLICY.setRolePermission( + actionCreatorRole, + ILlamaPolicy.PermissionData(address(POLICY), POLICY.initializeRole.selector, address(STRATEGY)), + true + ); + vm.stopPrank(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + } + + function createOffchainSignature(uint256 privateKey) internal view returns (uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = createOffchainSignatureWithDescription(privateKey, ""); + } + + function createOffchainSignatureWithDescription(uint256 privateKey, string memory description) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CreateActionBySig memory _createAction = LlamaCoreSigUtils.CreateActionBySig({ + role: actionCreatorRole, + strategy: address(STRATEGY), + target: address(POLICY), + value: 0, + data: abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + description: description, + tokenHolder: tokenHolder, + nonce: 0 + }); + bytes32 digest = getCreateActionBySigTypedDataHash(_createAction); + (v, r, s) = vm.sign(privateKey, digest); + } + + function createActionBySig(uint8 v, bytes32 r, bytes32 s) internal returns (uint256 actionId) { + actionId = actionCreator.createActionBySig( + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + "", + v, + r, + s + ); + } + + function test_CreatesActionBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + bytes memory data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + + uint256 actionCount = CORE.actionsCount(); + + vm.expectEmit(); + emit ActionCreated(actionCount, tokenHolder, actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + + uint256 actionId = createActionBySig(v, r, s); + Action memory action = CORE.getAction(actionId); + + assertEq(actionId, actionCount); + assertEq(CORE.actionsCount() - 1, actionCount); + assertEq(action.creationTime, block.timestamp); + } + + function test_CreatesActionBySigWithDescription() public { + (uint8 v, bytes32 r, bytes32 s) = + createOffchainSignatureWithDescription(tokenHolderPrivateKey, "# Action 0 \n This is my action."); + bytes memory data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + + uint256 actionCount = CORE.actionsCount(); + + vm.expectEmit(); + emit ActionCreated( + actionCount, + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + data, + "# Action 0 \n This is my action." + ); + + uint256 actionId = actionCreator.createActionBySig( + tokenHolder, + actionCreatorRole, + STRATEGY, + address(POLICY), + 0, + abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))), + "# Action 0 \n This is my action.", + v, + r, + s + ); + Action memory action = CORE.getAction(actionId); + + assertEq(actionId, actionCount); + assertEq(CORE.actionsCount() - 1, actionCount); + assertEq(action.creationTime, block.timestamp); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.createActionBySig.selector), 0); + createActionBySig(v, r, s); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.createActionBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + createActionBySig(v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as + // policyholder since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } + + function test_RevertIf_SignerIsNotPolicyHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the policyholder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig((v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(tokenHolderPrivateKey); + + vm.prank(tokenHolder); + actionCreator.incrementNonce(ILlamaCore.createActionBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + createActionBySig(v, r, s); + } +} + +contract CancelActionBySig is ERC721TokenholderActionCreatorTest, LlamaCoreSigUtils { + bytes data = abi.encodeCall(POLICY.initializeRole, (RoleDescription.wrap("Test Role"))); + uint256 actionId; + ERC721TokenholderActionCreator actionCreator; + ActionInfo actionInfo; + uint8 actionCreatorRole; + + function setUp() public virtual override { + ERC721TokenholderActionCreatorTest.setUp(); + mockErc721Votes.mint(address(tokenHolder), 0); // we use mockErc721Votes because IVotesToken is an + // interface without the `delegate` function + vm.prank(tokenHolder); + mockErc721Votes.delegate(tokenHolder); // we use mockErc721Votes because IVotesToken is an interface without + // the `delegate` function + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + + actionCreator = new ERC721TokenholderActionCreator(token, ILlamaCore(address(CORE)), 1); + + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(actionCreator) + }) + ); + + vm.startPrank(address(EXECUTOR)); // init role, assign policy, and assign permission to setRoleHolder to the token + // voting action creator + POLICY.initializeRole(RoleDescription.wrap("Token Voting Action Creator Role")); + actionCreatorRole = 2; + POLICY.setRoleHolder(actionCreatorRole, address(actionCreator), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + POLICY.setRolePermission( + actionCreatorRole, + ILlamaPolicy.PermissionData(address(POLICY), POLICY.initializeRole.selector, address(STRATEGY)), + true + ); + vm.stopPrank(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + + vm.expectEmit(); + emit ActionCreated(CORE.actionsCount(), tokenHolder, actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + vm.prank(tokenHolder); + actionId = actionCreator.createAction(actionCreatorRole, STRATEGY, address(POLICY), 0, data, ""); + + actionInfo = ActionInfo(actionId, address(actionCreator), actionCreatorRole, STRATEGY, address(POLICY), 0, data); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CancelActionBySig memory cancelAction = LlamaCoreSigUtils.CancelActionBySig({ + tokenHolder: tokenHolder, + actionInfo: _actionInfo, + nonce: actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector) + }); + bytes32 digest = getCancelActionBySigTypedDataHash(cancelAction); + (v, r, s) = vm.sign(privateKey, digest); + } + + function cancelActionBySig(ActionInfo memory _actionInfo, uint8 v, bytes32 r, bytes32 s) internal { + actionCreator.cancelActionBySig(tokenHolder, _actionInfo, v, r, s); + } + + function test_CancelActionBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + // vm.expectEmit(); + // emit ActionCanceled(actionInfo.id, tokenHolder); + + cancelActionBySig(actionInfo, v, r, s); + + uint256 state = uint256(CORE.getActionState(actionInfo)); + uint256 canceled = uint256(ActionState.Canceled); + assertEq(state, canceled); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector), 0); + cancelActionBySig(actionInfo, v, r, s); + assertEq(actionCreator.nonces(tokenHolder, ILlamaCore.cancelActionBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + cancelActionBySig(actionInfo, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsNotTokenHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the policyholder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, (v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolderPrivateKey); + + vm.prank(tokenHolder); + actionCreator.incrementNonce(ILlamaCore.cancelActionBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + cancelActionBySig(actionInfo, v, r, s); + } +} diff --git a/test/token-voting/ERC721TokenholderCaster.t.sol b/test/token-voting/ERC721TokenholderCaster.t.sol index d345d4a..e20b950 100644 --- a/test/token-voting/ERC721TokenholderCaster.t.sol +++ b/test/token-voting/ERC721TokenholderCaster.t.sol @@ -5,6 +5,7 @@ import {Test, console2} from "forge-std/Test.sol"; import {MockERC721Votes} from "test/mock/MockERC721Votes.sol"; +import {ActionState} from "src/lib/Enums.sol"; import {Action, ActionInfo, PermissionData} from "src/lib/Structs.sol"; import {ILlamaCore} from "src/interfaces/ILlamaCore.sol"; import {ILlamaPolicy} from "src/interfaces/ILlamaPolicy.sol"; @@ -15,6 +16,7 @@ import {ERC721Votes} from "lib/openzeppelin-contracts/contracts/token/ERC721/ext import {ERC721TokenholderCaster} from "src/token-voting/ERC721TokenholderCaster.sol"; import {TokenholderCaster} from "src/token-voting/TokenholderCaster.sol"; import {PeripheryTestSetup} from "test/PeripheryTestSetup.sol"; +import {LlamaCoreSigUtils} from "test/utils/LlamaCoreSigUtils.sol"; contract ERC721TokenholderCasterTest is PeripheryTestSetup { uint256 constant DEFAULT_APPROVAL_THRESHOLD = 2; @@ -33,7 +35,8 @@ contract ERC721TokenholderCasterTest is PeripheryTestSetup { ILlamaStrategy tokenVotingStrategy; - address tokenHolder1 = makeAddr("tokenholder-1"); + address tokenHolder1; + uint256 tokenHolder1PrivateKey; address tokenHolder2 = makeAddr("tokenholder-2"); address tokenHolder3 = makeAddr("tokenholder-3"); @@ -108,6 +111,7 @@ contract ERC721TokenholderCasterTest is PeripheryTestSetup { function setUp() public virtual override { PeripheryTestSetup.setUp(); + (tokenHolder1, tokenHolder1PrivateKey) = makeAddrAndKey("tokenholder-1"); vm.deal(address(this), 1 ether); vm.deal(address(msg.sender), 1 ether); vm.deal(address(EXECUTOR), 1 ether); @@ -121,7 +125,7 @@ contract ERC721TokenholderCasterTest is PeripheryTestSetup { vm.prank(address(EXECUTOR)); POLICY.initializeRole(RoleDescription.wrap("Token Voting Caster Role")); // initializes role 2 vm.prank(address(EXECUTOR)); - POLICY.initializeRole(RoleDescription.wrap("Made Up Role")); // initializes role 2 + POLICY.initializeRole(RoleDescription.wrap("Made Up Role")); // initializes role 3 mockErc721Votes.mint(tokenHolder1, 0); mockErc721Votes.mint(tokenHolder2, 1); @@ -540,3 +544,226 @@ contract SubmitDisapprovals is ERC721TokenholderCasterTest { caster.submitDisapprovals(actionInfo); } } + +contract CastApprovalBySig is ERC721TokenholderCasterTest, LlamaCoreSigUtils { + function setUp() public virtual override { + ERC721TokenholderCasterTest.setUp(); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(caster) + }) + ); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CastApprovalBySig memory castApproval = LlamaCoreSigUtils.CastApprovalBySig({ + actionInfo: _actionInfo, + support: 1, + reason: "", + tokenHolder: tokenHolder1, + nonce: 0 + }); + bytes32 digest = getCastApprovalBySigTypedDataHash(castApproval); + (v, r, s) = vm.sign(privateKey, digest); + } + + function castApprovalBySig(ActionInfo memory _actionInfo, uint8 support, uint8 v, bytes32 r, bytes32 s) internal { + caster.castApprovalBySig(tokenHolder1, support, _actionInfo, "", v, r, s); + } + + function test_CastsApprovalBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.expectEmit(); + emit ApprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, block.timestamp - 1), "" + ); + + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + assertEq(caster.nonces(tokenHolder1, TokenholderCaster.castApprovalBySig.selector), 0); + castApprovalBySig(actionInfo, 1, v, r, s); + assertEq(caster.nonces(tokenHolder1, TokenholderCaster.castApprovalBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + castApprovalBySig(actionInfo, 1, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(TokenholderCaster.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_RevertIf_SignerIsNotTokenHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address is not the same as the token holder passed in as + // parameter. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, (v + 1), r, s); + } + + function test_RevertIf_TokenHolderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.prank(tokenHolder1); + caster.incrementNonce(ILlamaCore.castApprovalBySig.selector); + + // Invalid Signature error since the recovered signer address during the call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castApprovalBySig(actionInfo, 1, v, r, s); + } +} + +contract CastDisapprovalBySig is ERC721TokenholderCasterTest, LlamaCoreSigUtils { + function setUp() public virtual override { + ERC721TokenholderCasterTest.setUp(); + + castApprovalsFor(); + + vm.warp(block.timestamp + (1 days * TWO_THIRDS_IN_BPS) / ONE_HUNDRED_IN_BPS); + + vm.prank(tokenHolder1); + caster.submitApprovals(actionInfo); + + // Setting Mock Protocol Core's EIP-712 Domain Hash + setDomainHash( + LlamaCoreSigUtils.EIP712Domain({ + name: CORE.name(), + version: "1", + chainId: block.chainid, + verifyingContract: address(caster) + }) + ); + } + + function createOffchainSignature(ActionInfo memory _actionInfo, uint256 privateKey) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CastDisapprovalBySig memory castDisapproval = LlamaCoreSigUtils.CastDisapprovalBySig({ + actionInfo: _actionInfo, + support: 1, + reason: "", + tokenHolder: tokenHolder1, + nonce: 0 + }); + bytes32 digest = getCastDisapprovalBySigTypedDataHash(castDisapproval); + (v, r, s) = vm.sign(privateKey, digest); + } + + function castDisapprovalBySig(ActionInfo memory _actionInfo, uint8 v, bytes32 r, bytes32 s) internal { + caster.castDisapprovalBySig(tokenHolder1, 1, _actionInfo, "", v, r, s); + } + + function test_CastsDisapprovalBySig() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.expectEmit(); + emit DisapprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, token.clock() - 1), "" + ); + + castDisapprovalBySig(actionInfo, v, r, s); + + // assertEq(CORE.getAction(0).totalDisapprovals, 1); + // assertEq(CORE.disapprovals(0, disapproverDrake), true); + } + + function test_CheckNonceIncrements() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + assertEq(caster.nonces(tokenHolder1, ILlamaCore.castDisapprovalBySig.selector), 0); + castDisapprovalBySig(actionInfo, v, r, s); + assertEq(caster.nonces(tokenHolder1, ILlamaCore.castDisapprovalBySig.selector), 1); + } + + function test_OperationCannotBeReplayed() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + castDisapprovalBySig(actionInfo, v, r, s); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsNotPolicyHolder() public { + (, uint256 randomSignerPrivateKey) = makeAddrAndKey("randomSigner"); + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, randomSignerPrivateKey); + // Invalid Signature error since the recovered signer address during the second call is not the same as token holder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_RevertIf_SignerIsZeroAddress() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + // Invalid Signature error since the recovered signer address is zero address due to invalid signature values + // (v,r,s). + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, (v + 1), r, s); + } + + function test_RevertIf_PolicyholderIncrementsNonce() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + vm.prank(tokenHolder1); + caster.incrementNonce(ILlamaCore.castDisapprovalBySig.selector); + + // Invalid Signature error since the recovered signer address during the second call is not the same as policyholder + // since nonce has increased. + vm.expectRevert(ILlamaCore.InvalidSignature.selector); + castDisapprovalBySig(actionInfo, v, r, s); + } + + function test_FailsIfDisapproved() public { + (uint8 v, bytes32 r, bytes32 s) = createOffchainSignature(actionInfo, tokenHolder1PrivateKey); + + // First disapproval. + vm.expectEmit(); + emit DisapprovalCast( + actionInfo.id, tokenHolder1, CASTER_ROLE, 1, token.getPastVotes(tokenHolder1, token.clock() - 1), "" + ); + castDisapprovalBySig(actionInfo, v, r, s); + // assertEq(CORE.getAction(actionInfo.id).totalDisapprovals, 1); + + // Second disapproval. + vm.prank(tokenHolder2); + caster.castDisapproval(actionInfo, 1, ""); + + vm.warp(block.timestamp + 1 + (1 days * TWO_THIRDS_IN_BPS) / ONE_HUNDRED_IN_BPS); + + caster.submitDisapprovals(actionInfo); + + // Assertions. + ActionState state = ActionState(CORE.getActionState(actionInfo)); + assertEq(uint8(state), uint8(ActionState.Failed)); + + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Failed)); + CORE.executeAction(actionInfo); + } +} diff --git a/test/token-voting/LlamaTokenVotingFactory.t.sol b/test/token-voting/LlamaTokenVotingFactory.t.sol index 73b9354..b3a40f7 100644 --- a/test/token-voting/LlamaTokenVotingFactory.t.sol +++ b/test/token-voting/LlamaTokenVotingFactory.t.sol @@ -118,4 +118,3 @@ contract DeployTokenVotingModule is LlamaTokenVotingFactoryTest { CORE.executeAction(actionInfo); } } - diff --git a/test/utils/LlamaCoreSigUtils.sol b/test/utils/LlamaCoreSigUtils.sol new file mode 100644 index 0000000..3c752c9 --- /dev/null +++ b/test/utils/LlamaCoreSigUtils.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ActionInfo} from "src/lib/Structs.sol"; + +contract LlamaCoreSigUtils { + struct EIP712Domain { + string name; + string version; + uint256 chainId; + address verifyingContract; + } + + struct CreateAction { + address policyholder; + uint8 role; + address strategy; + address target; + uint256 value; + bytes data; + string description; + uint256 nonce; + } + + struct CreateActionBySig { + address tokenHolder; + uint8 role; + address strategy; + address target; + uint256 value; + bytes data; + string description; + uint256 nonce; + } + + struct CancelAction { + address policyholder; + ActionInfo actionInfo; + uint256 nonce; + } + + struct CancelActionBySig { + address tokenHolder; + ActionInfo actionInfo; + uint256 nonce; + } + + struct CastApproval { + address policyholder; + uint8 role; + ActionInfo actionInfo; + string reason; + uint256 nonce; + } + + struct CastDisapproval { + address policyholder; + uint8 role; + ActionInfo actionInfo; + string reason; + uint256 nonce; + } + + struct CastApprovalBySig { + address tokenHolder; + uint8 support; + ActionInfo actionInfo; + string reason; + uint256 nonce; + } + + struct CastDisapprovalBySig { + address tokenHolder; + uint8 support; + ActionInfo actionInfo; + string reason; + uint256 nonce; + } + + /// @notice EIP-712 base typehash. + bytes32 internal constant EIP712_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @notice EIP-712 createAction typehash. + bytes32 internal constant CREATE_ACTION_TYPEHASH = keccak256( + "CreateAction(address policyholder,uint8 role,address strategy,address target,uint256 value,bytes data,string description,uint256 nonce)" + ); + + /// @notice EIP-712 createAction typehash. + bytes32 internal constant CREATE_ACTION_BY_SIG_TYPEHASH = keccak256( + "CreateAction(address tokenHolder,uint8 role,address strategy,address target,uint256 value,bytes data,string description,uint256 nonce)" + ); + + /// @dev EIP-712 cancelAction typehash. + bytes32 internal constant CANCEL_ACTION_TYPEHASH = keccak256( + "CancelAction(address policyholder,ActionInfo actionInfo,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @dev EIP-712 cancelAction typehash. + bytes32 internal constant CANCEL_ACTION_BY_SIG_TYPEHASH = keccak256( + "CancelAction(address tokenHolder,ActionInfo actionInfo,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @notice EIP-712 castApproval typehash. + bytes32 internal constant CAST_APPROVAL_TYPEHASH = keccak256( + "CastApproval(address policyholder,uint8 role,ActionInfo actionInfo,string reason,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @notice EIP-712 castApproval typehash. + bytes32 internal constant CAST_APPROVAL_BY_SIG_TYPEHASH = keccak256( + "CastApproval(address tokenHolder,uint8 support,ActionInfo actionInfo,string reason,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @notice EIP-712 castDisapproval typehash. + bytes32 internal constant CAST_DISAPPROVAL_TYPEHASH = keccak256( + "CastDisapproval(address policyholder,uint8 role,ActionInfo actionInfo,string reason,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @notice EIP-712 castDisapproval typehash. + bytes32 internal constant CAST_DISAPPROVAL_BY_SIG_TYPEHASH = keccak256( + "CastDisapproval(address tokenHolder,uint8 role,ActionInfo actionInfo,string reason,uint256 nonce)ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + /// @notice EIP-712 actionInfo typehash. + bytes32 internal constant ACTION_INFO_TYPEHASH = keccak256( + "ActionInfo(uint256 id,address creator,uint8 creatorRole,address strategy,address target,uint256 value,bytes data)" + ); + + bytes32 internal DOMAIN_SEPARATOR; + + /// @notice Sets the EIP-712 domain separator. + function setDomainHash(EIP712Domain memory eip712Domain) internal { + DOMAIN_SEPARATOR = keccak256( + abi.encode( + EIP712_DOMAIN_TYPEHASH, + keccak256(bytes(eip712Domain.name)), + keccak256(bytes(eip712Domain.version)), + eip712Domain.chainId, + eip712Domain.verifyingContract + ) + ); + } + + /// @notice Returns the hash of CreateAction. + function getCreateActionHash(CreateAction memory createAction) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CREATE_ACTION_TYPEHASH, + createAction.policyholder, + createAction.role, + createAction.strategy, + createAction.target, + createAction.value, + keccak256(createAction.data), + keccak256(bytes(createAction.description)), + createAction.nonce + ) + ); + } + + /// @notice Returns the hash of CreateAction. + function getCreateActionBySigHash(CreateActionBySig memory createAction) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CREATE_ACTION_BY_SIG_TYPEHASH, + createAction.tokenHolder, + createAction.role, + createAction.strategy, + createAction.target, + createAction.value, + keccak256(createAction.data), + keccak256(bytes(createAction.description)), + createAction.nonce + ) + ); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CreateAction domain, which can be used to + /// recover the signer. + function getCreateActionTypedDataHash(CreateAction memory createAction) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCreateActionHash(createAction))); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CreateAction domain, which can be used to + /// recover the signer. + function getCreateActionBySigTypedDataHash(CreateActionBySig memory createAction) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCreateActionBySigHash(createAction))); + } + + /// @notice Returns the hash of CancelAction. + function getCancelActionHash(CancelAction memory cancelAction) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CANCEL_ACTION_TYPEHASH, + cancelAction.policyholder, + getActionInfoHash(cancelAction.actionInfo), + cancelAction.nonce + ) + ); + } + + /// @notice Returns the hash of CancelActionBySig. + function getCancelActionBySigHash(CancelActionBySig memory cancelAction) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CANCEL_ACTION_BY_SIG_TYPEHASH, + cancelAction.tokenHolder, + getActionInfoHash(cancelAction.actionInfo), + cancelAction.nonce + ) + ); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CancelAction domain, which can be used to + /// recover the signer. + function getCancelActionTypedDataHash(CancelAction memory cancelAction) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCancelActionHash(cancelAction))); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CancelActionBySig domain, which can be used + /// to + /// recover the signer. + function getCancelActionBySigTypedDataHash(CancelActionBySig memory cancelAction) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCancelActionBySigHash(cancelAction))); + } + + /// @notice Returns the hash of CastApproval. + function getCastApprovalHash(CastApproval memory castApproval) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CAST_APPROVAL_TYPEHASH, + castApproval.policyholder, + castApproval.role, + getActionInfoHash(castApproval.actionInfo), + keccak256(bytes(castApproval.reason)), + castApproval.nonce + ) + ); + } + + function getCastApprovalBySigHash(CastApprovalBySig memory castApproval) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CAST_APPROVAL_BY_SIG_TYPEHASH, + castApproval.tokenHolder, + castApproval.support, + getActionInfoHash(castApproval.actionInfo), + keccak256(bytes(castApproval.reason)), + castApproval.nonce + ) + ); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CastApproval domain, which can be used to + /// recover the signer. + function getCastApprovalTypedDataHash(CastApproval memory castApproval) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCastApprovalHash(castApproval))); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CastApprovalBySig domain, which can be used + /// to + /// recover the signer. + function getCastApprovalBySigTypedDataHash(CastApprovalBySig memory castApproval) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCastApprovalBySigHash(castApproval))); + } + + /// @notice Returns the hash of CastDisapproval. + function getCastDisapprovalHash(CastDisapproval memory castDisapproval) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CAST_DISAPPROVAL_TYPEHASH, + castDisapproval.policyholder, + castDisapproval.role, + getActionInfoHash(castDisapproval.actionInfo), + keccak256(bytes(castDisapproval.reason)), + castDisapproval.nonce + ) + ); + } + + /// @notice Returns the hash of CastDisapprovalBySig. + function getCastDisapprovalBySigHash(CastDisapprovalBySig memory castDisapproval) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CAST_DISAPPROVAL_BY_SIG_TYPEHASH, + castDisapproval.tokenHolder, + castDisapproval.support, + getActionInfoHash(castDisapproval.actionInfo), + keccak256(bytes(castDisapproval.reason)), + castDisapproval.nonce + ) + ); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CastDisapproval domain, which can be used to + /// recover the signer. + function getCastDisapprovalTypedDataHash(CastDisapproval memory castDisapproval) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCastDisapprovalHash(castDisapproval))); + } + + /// @notice Returns the hash of the fully encoded EIP-712 message for the CastDisapprovalBySig domain, which can be + /// used to + /// recover the signer. + function getCastDisapprovalBySigTypedDataHash(CastDisapprovalBySig memory castDisapproval) + internal + view + returns (bytes32) + { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getCastDisapprovalBySigHash(castDisapproval))); + } + + /// @notice Returns the hash of ActionInfo. + function getActionInfoHash(ActionInfo memory actionInfo) internal pure returns (bytes32) { + return keccak256( + abi.encode( + ACTION_INFO_TYPEHASH, + actionInfo.id, + actionInfo.creator, + actionInfo.creatorRole, + address(actionInfo.strategy), + actionInfo.target, + actionInfo.value, + keccak256(actionInfo.data) + ) + ); + } +}