From 76e6bc2d6fe595f5728d32a2d55a3e5002317866 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 6 May 2025 13:05:08 -0300 Subject: [PATCH 01/73] feat: add proposal validator (#367) * feat: add initial interface and logic * refactor: remove installed governor submodule * chore: remove xERC20 * feat: add proposal routing full flow * feat: check voting power and required proposals * refactor: rename to ProposalValidator * feat: add EAS validation for certain Proposal Types * chore: fix attestation schema approved address naming Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: remove management functions * chore: run pre-pr * refacto: follow style guide for function parameters and return variables * docs: add natspec, remove unused errors * chore: remove management functions from interface * chore: make voting token immutable * perf: make governor immutable * feat: add validator management functions * chore: add comments for imports in ProposalValidator * test: add unit tests * fix: semgrep warnings * chore: rename MaintenanceUpgradeProposals --> MaintenanceUpgrade * chore(semgrep): add excluded governance files * chore: fix coding style * chore: add ImmutableProposalTypeData * chore: improve errors naming * docs: improve natspec Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * docs: add technical explanation on attestation validation function * feat: add _proposalTypeData mapping * chore: keep private functions consistency * chore: improve required attestation naming * docs: improve documents legibility Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: fix immutable variables coding style * chore: explain governor external call * docs: explicit Validator/Governor contract interaction in events natspec --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .semgrep/rules/sol-rules.yaml | 6 + .../governance/IOptimismGovernor.sol | 20 + .../governance/IProposalValidator.sol | 125 +++++ .../snapshots/abi/ProposalValidator.json | 527 ++++++++++++++++++ .../storageLayout/ProposalValidator.json | 58 ++ .../src/governance/ProposalValidator.sol | 405 ++++++++++++++ .../src/governance/VotingModule.sol | 73 +++ .../test/governance/ProposalValidator.t.sol | 441 +++++++++++++++ 8 files changed, 1655 insertions(+) create mode 100644 packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol create mode 100644 packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol create mode 100644 packages/contracts-bedrock/snapshots/abi/ProposalValidator.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json create mode 100644 packages/contracts-bedrock/src/governance/ProposalValidator.sol create mode 100644 packages/contracts-bedrock/src/governance/VotingModule.sol create mode 100644 packages/contracts-bedrock/test/governance/ProposalValidator.t.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index ec2fbc04b39..a4f25a2e351 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -125,6 +125,7 @@ rules: - packages/contracts-bedrock/src/universal/WETH98.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol - packages/contracts-bedrock/src/governance/GovernanceToken.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -139,6 +140,7 @@ rules: - packages/contracts-bedrock/test/safe-tools - packages/contracts-bedrock/scripts/libraries/Solarray.sol - packages/contracts-bedrock/scripts/interfaces/IGnosisSafe.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - id: sol-style-doc-comment languages: [solidity] @@ -241,6 +243,7 @@ rules: - packages/contracts-bedrock/src/libraries/Blueprint.sol - packages/contracts-bedrock/src/dispute/lib/Errors.sol - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol @@ -341,6 +344,9 @@ rules: - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol - packages/contracts-bedrock/src/governance/MintManager.sol + - packages/contracts-bedrock/src/governance/ProposalValidator.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ProposalValidator.sol - packages/contracts-bedrock/src/periphery/TransferOnion.sol - packages/contracts-bedrock/src/periphery/faucet/Faucet.sol - packages/contracts-bedrock/src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol new file mode 100644 index 00000000000..fd9e773b466 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {VotingModule} from "src/governance/VotingModule.sol"; +interface IOptimismGovernor { + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint8 proposalType + ) external returns (uint256 proposalId); + + function proposeWithModule( + VotingModule module, + bytes memory proposalData, + string memory description, + uint8 proposalType + ) external returns (uint256 proposalId); +} \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol new file mode 100644 index 00000000000..caaf75aee42 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGovernanceToken} from "./IGovernanceToken.sol"; +import {IOptimismGovernor} from "./IOptimismGovernor.sol"; + +/// @title IProposalValidator +/// @notice Interface for the ProposalValidator contract. +interface IProposalValidator { + error ProposalValidator_InsufficientApprovals(); + error ProposalValidator_ProposalAlreadyApproved(); + error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_InsufficientVotingPower(); + error ProposalValidator_InvalidAttestation(); + + struct ProposalData { + address proposer; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalType proposalType; + bool inVoting; + mapping(address => bool) delegateApprovals; + uint256 remainingApprovalsRequired; + } + + struct ImmutableProposalTypeData { + address[] targets; + uint256[] values; + string[] signatures; + } + + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } + + event ProposalSubmitted( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalType proposalType + ); + + event ProposalApproved( + uint256 indexed proposalId, + address indexed approver + ); + + event ProposalMovedToVote( + uint256 indexed proposalId, + address indexed executor + ); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + + event VotingCycleBlockSet(uint256 newVotingCycleBlock); + + event DistributionThresholdSet(uint256 newDistributionThreshold); + + event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + function submitProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description, + ProposalType _proposalType, + bytes32 _attestationUid + ) external returns (uint256 proposalId_); + + function approveProposal(uint256 _proposalId) external; + + function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_); + + function setMinimumVotingPower(uint256 _minimumVotingPower) external; + + function setVotingCycleBlock(uint256 _votingCycleBlock) external; + + function setDistributionThreshold(uint256 _distributionThreshold) external; + + function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function renounceOwnership() external; + + function canSignOff(address _delegate) external view returns (bool canSignOff_); + + function transferOwnership(address newOwner) external; + + function minimumVotingPower() external view returns (uint256); + + function votingCycleBlock() external view returns (uint256); + + function distributionThreshold() external view returns (uint256); + + function VOTING_TOKEN() external view returns (IGovernanceToken); + + function GOVERNOR() external view returns (IOptimismGovernor); + + function owner() external view returns (address); + + function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function __constructor__( + address _owner, + IOptimismGovernor _governor, + IGovernanceToken _votingToken, + bytes32 _attestationSchemaUid, + uint256 _minimumVotingPower, + uint256 _votingCycleBlock, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals, + ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) external; +} diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json new file mode 100644 index 00000000000..8044503e6fc --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -0,0 +1,527 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "contract IOptimismGovernor", + "name": "_governor", + "type": "address" + }, + { + "internalType": "contract IGovernanceToken", + "name": "_votingToken", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_attestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + }, + { + "internalType": "enum ProposalValidator.ProposalType[]", + "name": "_proposalTypes", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "_requiredApprovals", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + } + ], + "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", + "name": "_immutableProposalTypeDatas", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GOVERNOR", + "outputs": [ + { + "internalType": "contract IOptimismGovernor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VOTING_TOKEN", + "outputs": [ + { + "internalType": "contract IGovernanceToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "approveProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_delegate", + "type": "address" + } + ], + "name": "canSignOff", + "outputs": [ + { + "internalType": "bool", + "name": "canSignOff_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "distributionThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minimumVotingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "moveToVote", + "outputs": [ + { + "internalType": "uint256", + "name": "governorProposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + } + ], + "name": "setDistributionThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + } + ], + "name": "setMinimumVotingPower", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_requiredApprovals", + "type": "uint256" + } + ], + "name": "setProposalRequiredApprovals", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + } + ], + "name": "setVotingCycleBlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_calldatas", + "type": "bytes[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + } + ], + "name": "submitProposal", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "votingCycleBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newDistributionThreshold", + "type": "uint256" + } + ], + "name": "DistributionThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newMinimumVotingPower", + "type": "uint256" + } + ], + "name": "MinimumVotingPowerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newApprovalThreshold", + "type": "uint256" + } + ], + "name": "ProposalApprovalThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ProposalApproved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "executor", + "type": "address" + } + ], + "name": "ProposalMovedToVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + }, + { + "indexed": false, + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + } + ], + "name": "ProposalSubmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newVotingCycleBlock", + "type": "uint256" + } + ], + "name": "VotingCycleBlockSet", + "type": "event" + }, + { + "inputs": [], + "name": "ProposalValidator_InsufficientApprovals", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InsufficientVotingPower", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidAttestation", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyApproved", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyInVoting", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json new file mode 100644 index 00000000000..4050cff3575 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -0,0 +1,58 @@ +[ + { + "bytes": "20", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "address" + }, + { + "bytes": "32", + "label": "minimumVotingPower", + "offset": 0, + "slot": "1", + "type": "uint256" + }, + { + "bytes": "32", + "label": "votingCycleBlock", + "offset": 0, + "slot": "2", + "type": "uint256" + }, + { + "bytes": "32", + "label": "distributionThreshold", + "offset": 0, + "slot": "3", + "type": "uint256" + }, + { + "bytes": "32", + "label": "_proposalRequiredApprovals", + "offset": 0, + "slot": "4", + "type": "mapping(enum ProposalValidator.ProposalType => uint256)" + }, + { + "bytes": "32", + "label": "_proposalTypeData", + "offset": 0, + "slot": "5", + "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" + }, + { + "bytes": "32", + "label": "_proposals", + "offset": 0, + "slot": "6", + "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" + }, + { + "bytes": "32", + "label": "_proposalCounter", + "offset": 0, + "slot": "7", + "type": "uint256" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol new file mode 100644 index 00000000000..0be3d7dbe11 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; + +/// @title ProposalValidator +/// @notice The ProposalValidator contract is responsible for validating proposals and moving +/// them to the vote phase on the Optimism Governor. +contract ProposalValidator is Ownable { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when a proposal doesn't have enough delegate approvals to move to vote. + error ProposalValidator_InsufficientApprovals(); + /// @notice Thrown when a delegate attempts to approve a proposal they've already approved. + error ProposalValidator_ProposalAlreadyApproved(); + + /// @notice Thrown when attempting to move a proposal to vote that is already in voting. + error ProposalValidator_ProposalAlreadyInVoting(); + + /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. + error ProposalValidator_InsufficientVotingPower(); + + /// @notice Thrown when an invalid attestation is provided for a proposal. + error ProposalValidator_InvalidAttestation(); + + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Data structure for storing proposal information. + /// @param proposer The address that submitted the proposal. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param calldatas Function data for proposal calls. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal from the ProposalType enum. + /// @param inVoting Whether the proposal has been moved to the voting phase. + /// @param delegateApprovals Mapping of delegate addresses to their approval status. + /// @param remainingApprovalsRequired Number of approvals still needed before being able to move for voting. + struct ProposalData { + address proposer; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalType proposalType; + bool inVoting; + mapping(address => bool) delegateApprovals; + uint256 remainingApprovalsRequired; + } + + /// @notice Data structure for storing immutable proposal type data. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param signatures Function signatures for proposal calls. + struct ImmutableProposalTypeData { + address[] targets; + uint256[] values; + string[] signatures; + } + + /*////////////////////////////////////////////////////////////// + ENUMS + //////////////////////////////////////////////////////////////*/ + + /// @notice Types of proposals that can be submitted. + /// @param ProtocolOrGovernorUpgrade Proposals for upgrading the protocol or governor. + /// @param MaintenanceUpgrade Proposals for maintenance upgrades. + /// @param CouncilMemberElections Proposals for council member elections. + /// @param GovernanceFund Proposals related to the governance fund. + /// @param CouncilBudget Proposals related to the council budget. + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new proposal is submitted to the validator contract. + /// @param proposalId The ID of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param calldatas Function data for proposal calls. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalType proposalType + ); + + /// @notice Emitted when a delegate approves a proposal. + /// @param proposalId The ID of the approved proposal. + /// @param approver The address of the delegate who approved the proposal. + event ProposalApproved(uint256 indexed proposalId, address indexed approver); + + /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. + /// @param proposalId The ID of the proposal moved to vote. + /// @param executor The address that executed the move to vote. + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); + + /// @notice Emitted when the minimum voting power is set. + /// @param newMinimumVotingPower The new minimum voting power. + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + + /// @notice Emitted when the voting cycle block is set. + /// @param newVotingCycleBlock The new voting cycle block. + event VotingCycleBlockSet(uint256 newVotingCycleBlock); + + /// @notice Emitted when the distribution threshold is set. + /// @param newDistributionThreshold The new distribution threshold. + event DistributionThresholdSet(uint256 newDistributionThreshold); + + /// @notice Emitted when the number of approvals required for a proposal type is set. + /// @param proposalType The type of proposal. + /// @param newApprovalThreshold The new approval threshold. + event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + /// @notice The schema UID for attestations in the Ethereum Attestation Service. + /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } + bytes32 public immutable ATTESTATION_SCHEMA_UID; + + /// @notice The Optimism Governor contract that will handle the voting phase. + IOptimismGovernor public immutable GOVERNOR; + + /// @notice The token used to determine voting power. + IGovernanceToken public immutable VOTING_TOKEN; + + /// @notice The minimum voting power required for a delegate to approve proposals. + uint256 public minimumVotingPower; + + /// @notice The block number of the current voting cycle. + uint256 public votingCycleBlock; + + /// @notice The max amount of tokens that can be distributed in a proposal. + uint256 public distributionThreshold; + + /// @notice The number of approvals required for each proposal type. + mapping(ProposalType => uint256) private _proposalRequiredApprovals; + + /// @notice The immutable data for each proposal type. + mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; + + /// @notice Mapping of proposal IDs to their corresponding proposal data. + mapping(uint256 => ProposalData) private _proposals; + + /// @notice Counter for generating unique proposal IDs. + uint256 private _proposalCounter; + + /// @notice Initializes the ProposalValidator contract. + /// @param _owner The address that will own the contract. + /// @param _governor The Optimism Governor contract address. + /// @param _votingToken The token used to determine voting power. + /// @param _attestationSchemaUid The schema UID for attestations in EAS. + /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. + /// @param _votingCycleBlock The block number of the current voting cycle. + /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. + /// @param _proposalTypes Array of proposal types to set approval thresholds for. + /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. + /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. + constructor( + address _owner, + IOptimismGovernor _governor, + IGovernanceToken _votingToken, + bytes32 _attestationSchemaUid, + uint256 _minimumVotingPower, + uint256 _votingCycleBlock, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals, + ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) { + transferOwnership(_owner); + GOVERNOR = _governor; + VOTING_TOKEN = _votingToken; + ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + + _setMinimumVotingPower(_minimumVotingPower); + _setVotingCycleBlock(_votingCycleBlock); + _setDistributionThreshold(_distributionThreshold); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); + _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; + } + } + + /// @notice Submit a proposal for delegate approval. + /// @param _targets Target addresses for proposal calls. + /// @param _values ETH values for proposal calls. + /// @param _calldatas Function data for proposal calls. + /// @param _description Description of the proposal. + /// @param _proposalType Type of the proposal. + /// @param _attestationUid The UID of the attestation proving eligibility. + /// @return proposalId_ The ID of the submitted proposal. + function submitProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description, + ProposalType _proposalType, + bytes32 _attestationUid + ) + external + returns (uint256 proposalId_) + { + _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); + + proposalId_ = ++_proposalCounter; + + ProposalData storage proposal = _proposals[proposalId_]; + proposal.proposer = msg.sender; + proposal.targets = _targets; + proposal.values = _values; + proposal.calldatas = _calldatas; + proposal.description = _description; + proposal.proposalType = _proposalType; + proposal.inVoting = false; + proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes + + emit ProposalSubmitted(proposalId_, msg.sender, _targets, _values, _calldatas, _description, _proposalType); + + return proposalId_; + } + + /// @notice Approve a proposal (only callable by delegates with sufficient voting power). + /// @param _proposalId The ID of the proposal to approve. + function approveProposal(uint256 _proposalId) external { + if (!canSignOff(msg.sender)) { + revert ProposalValidator_InsufficientVotingPower(); + } + + ProposalData storage proposal = _proposals[_proposalId]; + + if (proposal.delegateApprovals[msg.sender]) { + revert ProposalValidator_ProposalAlreadyApproved(); + } + + proposal.delegateApprovals[msg.sender] = true; + proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted + + emit ProposalApproved(_proposalId, msg.sender); + } + + /// @notice Move a proposal to voting phase after sufficient delegate approvals. + /// @dev After passing all checks, the proposal is submitted with a external call to the governor contract. + /// @param _proposalId The ID of the proposal to move to vote. + /// @return governorProposalId_ The proposal ID in the governor contract. + function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_) { + ProposalData storage proposal = _proposals[_proposalId]; + + if (proposal.remainingApprovalsRequired > 0) { + revert ProposalValidator_InsufficientApprovals(); + } + + if (proposal.inVoting) { + revert ProposalValidator_ProposalAlreadyInVoting(); + } + + proposal.inVoting = true; + + governorProposalId_ = GOVERNOR.propose( + proposal.targets, proposal.values, proposal.calldatas, proposal.description, uint8(proposal.proposalType) + ); + + emit ProposalMovedToVote(_proposalId, msg.sender); + + return governorProposalId_; + } + + /// @notice Returns whether a delegate has enough voting power to approve a proposal. + /// @param _delegate The address of the delegate to check. + /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. + function canSignOff(address _delegate) public view returns (bool canSignOff_) { + return VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; + } + + /// @notice Sets the minimum voting power required for a delegate to approve proposals. + /// @param _minimumVotingPower The new minimum voting power threshold. + function setMinimumVotingPower(uint256 _minimumVotingPower) external onlyOwner { + _setMinimumVotingPower(_minimumVotingPower); + } + + /// @notice Sets the block number of the current voting cycle. + /// @param _votingCycleBlock The new voting cycle block number. + function setVotingCycleBlock(uint256 _votingCycleBlock) external onlyOwner { + _setVotingCycleBlock(_votingCycleBlock); + } + + /// @notice Sets the max amount of tokens that can be distributed in a proposal. + /// @param _distributionThreshold The new distribution threshold. + function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { + _setDistributionThreshold(_distributionThreshold); + } + + /// @notice Sets the number of approvals required for each proposal type. + /// @param _proposalType The type of proposal to set the required approvals for. + /// @param _requiredApprovals The new required approvals. + function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external onlyOwner { + _setProposalRequiredApprovals(_proposalType, _requiredApprovals); + } + + /// @notice Validates a proposal before submission. + /// @dev Checks if the proposal requires approval and validates the attestation. + /// @param _targets Target addresses for proposal calls. + /// @param _values ETH values for proposal calls. + /// @param _calldatas Function data for proposal calls. + /// @param _proposalType Type of the proposal. + /// @param _attestationUid The UID of the attestation proving eligibility. + function _validateProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + ProposalType _proposalType, + bytes32 _attestationUid + ) + private + view + { + if (_requiresAttestation(_proposalType)) { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + if ( + attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + || !_isValidAttestationData(attestation.data, _proposalType) + ) { + revert ProposalValidator_InvalidAttestation(); + } + } + } + + /// @notice Determines if a proposal type requires approval via attestation. + /// @param _proposalType The type of proposal to check. + /// @return requiresAttestation_ True if the proposal type requires approval, false otherwise. + function _requiresAttestation(ProposalType _proposalType) private pure returns (bool requiresAttestation_) { + return _proposalType == ProposalType.ProtocolOrGovernorUpgrade + || _proposalType == ProposalType.MaintenanceUpgrade || _proposalType == ProposalType.CouncilMemberElections; + } + + /// @notice Validates the attestation data for a proposal. + /// @dev Checks that the sender is the approved delegate and that the proposal type is correct. + /// @param _data The attestation data to validate. + /// @param _expectedProposalType The expected proposal type from the attestation. + /// @return isValid_ True if the attestation data is valid, false otherwise. + function _isValidAttestationData( + bytes memory _data, + ProposalType _expectedProposalType + ) + private + view + returns (bool isValid_) + { + (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); + return approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + } + + /// @notice Private function to set the minimum voting power and emit event. + /// @param _minimumVotingPower The new minimum voting power threshold. + function _setMinimumVotingPower(uint256 _minimumVotingPower) private { + minimumVotingPower = _minimumVotingPower; + emit MinimumVotingPowerSet(_minimumVotingPower); + } + + /// @notice Private function to set the voting cycle block and emit event. + /// @param _votingCycleBlock The new voting cycle block number. + function _setVotingCycleBlock(uint256 _votingCycleBlock) private { + votingCycleBlock = _votingCycleBlock; + emit VotingCycleBlockSet(_votingCycleBlock); + } + + /// @notice Private function to set the distribution threshold and emit event. + /// @param _distributionThreshold The new distribution threshold. + function _setDistributionThreshold(uint256 _distributionThreshold) private { + distributionThreshold = _distributionThreshold; + emit DistributionThresholdSet(_distributionThreshold); + } + + /// @notice Private function to set a proposal's type required approvals and emit event. + /// @param _proposalType The type of proposal to set the required approvals for. + /// @param _requiredApprovals The new required approvals. + function _setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) private { + _proposalRequiredApprovals[_proposalType] = _requiredApprovals; + emit ProposalApprovalThresholdSet(_proposalType, _requiredApprovals); + } +} diff --git a/packages/contracts-bedrock/src/governance/VotingModule.sol b/packages/contracts-bedrock/src/governance/VotingModule.sol new file mode 100644 index 00000000000..02b692d87f1 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/VotingModule.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +abstract contract VotingModule { + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + address immutable governor; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotGovernor(); // nosemgrep: + error ExistingProposal(); // nosemgrep: + error InvalidParams(); // nosemgrep: + error AlreadyVoted(); // nosemgrep: + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + function _onlyGovernor() internal view { + if (msg.sender != governor) revert NotGovernor(); + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) { + governor = _governor; + } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external virtual; + + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) + external + virtual; + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _formatExecuteParams( + uint256 proposalId, + bytes memory proposalData + ) + external + virtual + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas); + + function _voteSucceeded(uint256 /* proposalId */ ) external view virtual returns (bool) { + return true; + } + + function COUNTING_MODE() external pure virtual returns (string memory); + + function PROPOSAL_DATA_ENCODING() external pure virtual returns (string memory); + + function VOTE_PARAMS_ENCODING() external pure virtual returns (string memory); +} diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol new file mode 100644 index 00000000000..7e9c85a18f0 --- /dev/null +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Interfaces +import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; +import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; + +// Contracts +import { ProposalValidator } from "src/governance/ProposalValidator.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +/// @title ProposalValidator_Init +/// @notice Setup contract for ProposalValidator tests +contract ProposalValidator_Init is CommonTest { + uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP + uint256 public constant VOTING_CYCLE_BLOCK = 100; + uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; + uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; + uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + + address owner; + address rando; + address topDelegate_A; + address topDelegate_B; + address topDelegate_C; + address topDelegate_D; + + ProposalValidator public validator; + IOptimismGovernor public governor; + bytes32 public ATTESTATION_SCHEMA_UID; + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + /// @notice Helper function to make a top delegate. + function _makeTopDelegate(string memory _name) internal returns (address) { + address delegate = makeAddr(_name); + deal(address(governanceToken), delegate, TOP_DELEGATE_VOTING_POWER); + vm.prank(delegate); + governanceToken.delegate(delegate); + return delegate; + } + + /// @notice Helper function to make a (top) delegate approve a proposal. + function _approveProposal(address _delegate, uint256 _proposalId) internal { + vm.prank(_delegate); + validator.approveProposal(_proposalId); + } + + function _getProposalTypesRequiredApprovalsAndImmutableData() + internal + pure + returns ( + ProposalValidator.ProposalType[] memory, + uint256[] memory, + ProposalValidator.ImmutableProposalTypeData[] memory + ) + { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + proposalTypes[3] = ProposalValidator.ProposalType.GovernanceFund; + proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; + + uint256[] memory requiredApprovals = new uint256[](5); + requiredApprovals[0] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[1] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[2] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; + + ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData = + new ProposalValidator.ImmutableProposalTypeData[](5); + immutableProposalTypeData[0] = ProposalValidator.ImmutableProposalTypeData({ + targets: new address[](1), + values: new uint256[](1), + signatures: new string[](1) + }); + + return (proposalTypes, requiredApprovals, immutableProposalTypeData); + } + + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + owner = governanceToken.owner(); + rando = makeAddr("rando"); + governor = IOptimismGovernor(makeAddr("governor")); + + vm.prank(owner); + ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + ); + + ( + ProposalValidator.ProposalType[] memory proposalTypes, + uint256[] memory requiredApprovals, + ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData + ) = _getProposalTypesRequiredApprovalsAndImmutableData(); + + validator = new ProposalValidator( + owner, + governor, + governanceToken, + ATTESTATION_SCHEMA_UID, + MINIMUM_VOTING_POWER, + VOTING_CYCLE_BLOCK, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals, + immutableProposalTypeData + ); + + topDelegate_A = _makeTopDelegate("topDelegate_A"); + topDelegate_B = _makeTopDelegate("topDelegate_B"); + topDelegate_C = _makeTopDelegate("topDelegate_C"); + topDelegate_D = _makeTopDelegate("topDelegate_D"); + } + + /// @notice Helper to create a valid attestation for a proposal + function _createAttestation( + address _delegate, + ProposalValidator.ProposalType _proposalType + ) + internal + returns (bytes32) + { + vm.prank(owner); + return IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(_delegate, _proposalType), + value: 0 + }) + }) + ); + } + + /// @notice Helper to create a standard proposal setup + function _createProposalSetup() + internal + view + returns ( + address[] memory targets_, + uint256[] memory values_, + bytes[] memory calldatas_, + string memory description_ + ) + { + targets_ = new address[](1); + targets_[0] = address(0); + values_ = new uint256[](1); + values_[0] = 0; + calldatas_ = new bytes[](1); + calldatas_[0] = bytes(""); + description_ = "Test proposal"; + } +} + +/// @title ProposalValidator_SubmitProposal_Test +/// @notice Happy path tests for submitProposal function +contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { + function test_submitProposal_succeeds() public { + (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + uint256 proposalId = + validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); + + assertEq(proposalId, 1); + } +} + +/// @title ProposalValidator_SubmitProposal_TestFail +/// @notice Sad path tests for submitProposal function +contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { + function test_submitProposal_invalidAttestation_reverts() public { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID + + vm.prank(topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); + } + + function test_submitProposal_wrongAttester_reverts() public { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation with wrong delegate + bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); + + vm.prank(topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } +} + +/// @title ProposalValidator_ApproveProposal_Test +/// @notice Happy path tests for approveProposal function +contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + uint256 proposalId; + + function setUp() public override { + super.setUp(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_approveProposal_succeeds() public { + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + } +} + +/// @title ProposalValidator_ApproveProposal_TestFail +/// @notice Sad path tests for approveProposal function +contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { + uint256 proposalId; + + function setUp() public override { + super.setUp(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_approveProposal_insufficientVotingPower_reverts() public { + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); + _approveProposal(rando, proposalId); + } + + function test_approveProposal_alreadyApproved_reverts() public { + _approveProposal(topDelegate_A, proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); + _approveProposal(topDelegate_A, proposalId); + } +} + +/// @title ProposalValidator_MoveToVote_Test +/// @notice Happy path tests for moveToVote function +contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { + uint256 proposalId; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalValidator.ProposalType proposalType; + + function setUp() public override { + super.setUp(); + + (targets, values, calldatas, description) = _createProposalSetup(); + + proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + } + + function test_moveToVote_succeeds() public { + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + uint256 governorProposalId = validator.moveToVote(proposalId); + + assertEq(governorProposalId, 1); + } +} + +/// @title ProposalValidator_MoveToVote_TestFail +/// @notice Sad path tests for moveToVote function +contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { + uint256 proposalId; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalValidator.ProposalType proposalType; + + function setUp() public override { + super.setUp(); + + (targets, values, calldatas, description) = _createProposalSetup(); + + proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_moveToVote_insufficientApprovals_reverts() public { + // Only approve with 3 delegates (need 4) + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } + + function test_moveToVote_alreadyProposed_reverts() public { + // Approve with all 4 delegates + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + validator.moveToVote(proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } +} + +/// @title ProposalValidator_Getters_Test +/// @notice Tests for getter functions +contract ProposalValidator_Getters_Test is ProposalValidator_Init { + function test_canSignOff_succeeds() public { + bool canSignOff = validator.canSignOff(topDelegate_A); + assertTrue(canSignOff); + + bool cannotSignOff = validator.canSignOff(rando); + assertFalse(cannotSignOff); + } +} + +/// @title ProposalValidator_Setters_Test +/// @notice Tests for setter functions +contract ProposalValidator_Setters_Test is ProposalValidator_Init { +// TODO: Implement tests for setters +} + +/// @title ProposalValidator_Integration_Test +/// @notice Integration tests for the full proposal flow +contract ProposalValidator_Integration_Test is ProposalValidator_Init { + function test_proposalFullFlow_succeeds() public { + // Create a proposal + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + uint256 proposalId = + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + + assertEq(proposalId, 1); + + // It reverts when caller is not a top delegate + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); + _approveProposal(rando, proposalId); + + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + + // It reverts when proposal hasn't reached the required approvals + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + + _approveProposal(topDelegate_D, proposalId); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + validator.moveToVote(proposalId); + + // It reverts when proposal is already in voting phase + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } +} From ee2f4b1a68ceb32390fcaf3a1f84f81d7e2d5b2c Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 6 May 2025 14:47:08 -0300 Subject: [PATCH 02/73] feat: duplicated proposals check (#378) * feat: add initial interface and logic * refactor: remove installed governor submodule * chore: remove xERC20 * feat: add proposal routing full flow * feat: check voting power and required proposals * refactor: rename to ProposalValidator * feat: add EAS validation for certain Proposal Types * feat: add duplicated proposals validation * chore: fix attestation schema approved address naming Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: remove management functions * chore: run pre-pr * refacto: follow style guide for function parameters and return variables * docs: add natspec, remove unused errors * chore: remove management functions from interface * chore: make voting token immutable * perf: make governor immutable * feat: add validator management functions * chore: add comments for imports in ProposalValidator * test: add unit tests * chore: run pre-pr * fix: semgrep warnings * chore: rename MaintenanceUpgradeProposals --> MaintenanceUpgrade * chore(semgrep): add excluded governance files * chore: fix coding style * chore: add ImmutableProposalTypeData * chore: improve errors naming * docs: improve natspec Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * docs: add technical explanation on attestation validation function * feat: add _proposalTypeData mapping * chore: keep private functions consistency * chore: improve required attestation naming * chore: run pre-pr * chore: more descriptive errors * chore: confusing error name in submitProposal --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 29 ++-- .../snapshots/abi/ProposalValidator.json | 69 +++++--- .../storageLayout/ProposalValidator.json | 9 +- .../src/governance/ProposalValidator.sol | 152 +++++++++++------- .../test/governance/ProposalValidator.t.sol | 145 +++++++++-------- 5 files changed, 241 insertions(+), 163 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index caaf75aee42..4dc0cd45ad1 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -9,17 +9,15 @@ import {IOptimismGovernor} from "./IOptimismGovernor.sol"; interface IProposalValidator { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); - error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_ProposalAlreadySubmitted(); error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); + error ProposalValidator_ProposalDoesNotExist(); struct ProposalData { address proposer; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; ProposalType proposalType; + uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; @@ -40,22 +38,23 @@ interface IProposalValidator { } event ProposalSubmitted( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed proposer, address[] targets, uint256[] values, bytes[] calldatas, string description, - ProposalType proposalType + ProposalType proposalType, + uint8 proposalTypeConfigurator ); event ProposalApproved( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed approver ); event ProposalMovedToVote( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed executor ); @@ -75,12 +74,18 @@ interface IProposalValidator { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, + uint8 _proposalTypeConfigurator, bytes32 _attestationUid - ) external returns (uint256 proposalId_); + ) external returns (bytes32 proposalHash_); - function approveProposal(uint256 _proposalId) external; + function approveProposal(bytes32 _proposalHash) external; - function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_); + function moveToVote( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) external returns (uint256 governorProposalId_); function setMinimumVotingPower(uint256 _minimumVotingPower) external; diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 8044503e6fc..bdd01c8d1d9 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -114,9 +114,9 @@ { "inputs": [ { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "_proposalHash", + "type": "bytes32" } ], "name": "approveProposal", @@ -172,9 +172,24 @@ { "inputs": [ { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" + "internalType": "address[]", + "name": "_targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_calldatas", + "type": "bytes[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" } ], "name": "moveToVote", @@ -292,6 +307,11 @@ "name": "_proposalType", "type": "uint8" }, + { + "internalType": "uint8", + "name": "_proposalTypeConfigurator", + "type": "uint8" + }, { "internalType": "bytes32", "name": "_attestationUid", @@ -301,9 +321,9 @@ "name": "submitProposal", "outputs": [ { - "internalType": "uint256", - "name": "proposalId_", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" } ], "stateMutability": "nonpayable", @@ -404,9 +424,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -423,9 +443,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -442,9 +462,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -481,6 +501,12 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "proposalType", "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], "name": "ProposalSubmitted", @@ -521,7 +547,12 @@ }, { "inputs": [], - "name": "ProposalValidator_ProposalAlreadyInVoting", + "name": "ProposalValidator_ProposalAlreadySubmitted", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 4050cff3575..f7fdeefae8c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -46,13 +46,6 @@ "label": "_proposals", "offset": 0, "slot": "6", - "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" - }, - { - "bytes": "32", - "label": "_proposalCounter", - "offset": 0, - "slot": "7", - "type": "uint256" + "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 0be3d7dbe11..6661bc17030 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -22,11 +22,12 @@ contract ProposalValidator is Ownable { /// @notice Thrown when a proposal doesn't have enough delegate approvals to move to vote. error ProposalValidator_InsufficientApprovals(); + /// @notice Thrown when a delegate attempts to approve a proposal they've already approved. error ProposalValidator_ProposalAlreadyApproved(); /// @notice Thrown when attempting to move a proposal to vote that is already in voting. - error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_ProposalAlreadySubmitted(); /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. error ProposalValidator_InsufficientVotingPower(); @@ -34,27 +35,24 @@ contract ProposalValidator is Ownable { /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); + /// @notice Thrown when a proposal does not exist. + error ProposalValidator_ProposalDoesNotExist(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ /// @notice Data structure for storing proposal information. /// @param proposer The address that submitted the proposal. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param calldatas Function data for proposal calls. - /// @param description Description of the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. + /// @param proposalTypeConfigurator Configuration value specific to the proposal type. /// @param inVoting Whether the proposal has been moved to the voting phase. /// @param delegateApprovals Mapping of delegate addresses to their approval status. - /// @param remainingApprovalsRequired Number of approvals still needed before being able to move for voting. + /// @param remainingApprovalsRequired Number of approvals still needed before voting. struct ProposalData { address proposer; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; ProposalType proposalType; + uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; @@ -93,32 +91,34 @@ contract ProposalValidator is Ownable { //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a new proposal is submitted to the validator contract. - /// @param proposalId The ID of the submitted proposal. + /// @param proposalHash The hash of the submitted proposal. /// @param proposer The address that submitted the proposal. /// @param targets Target addresses for proposal calls. /// @param values ETH values for proposal calls. /// @param calldatas Function data for proposal calls. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. + /// @param proposalTypeConfigurator Configuration value specific to the proposal type. event ProposalSubmitted( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed proposer, address[] targets, uint256[] values, bytes[] calldatas, string description, - ProposalType proposalType + ProposalType proposalType, + uint8 proposalTypeConfigurator ); /// @notice Emitted when a delegate approves a proposal. - /// @param proposalId The ID of the approved proposal. + /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(uint256 indexed proposalId, address indexed approver); + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalId The ID of the proposal moved to vote. + /// @param proposalHash The hash of the proposal moved to vote. /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); /// @notice Emitted when the minimum voting power is set. /// @param newMinimumVotingPower The new minimum voting power. @@ -162,11 +162,8 @@ contract ProposalValidator is Ownable { /// @notice The immutable data for each proposal type. mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; - /// @notice Mapping of proposal IDs to their corresponding proposal data. - mapping(uint256 => ProposalData) private _proposals; - - /// @notice Counter for generating unique proposal IDs. - uint256 private _proposalCounter; + /// @notice Mapping of proposal hash to their corresponding proposal data. + mapping(bytes32 => ProposalData) private _proposals; /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. @@ -206,52 +203,60 @@ contract ProposalValidator is Ownable { } } - /// @notice Submit a proposal for delegate approval. - /// @param _targets Target addresses for proposal calls. - /// @param _values ETH values for proposal calls. - /// @param _calldatas Function data for proposal calls. - /// @param _description Description of the proposal. - /// @param _proposalType Type of the proposal. - /// @param _attestationUid The UID of the attestation proving eligibility. - /// @return proposalId_ The ID of the submitted proposal. + /// @notice Submit a proposal for delegate approval + /// @param _targets Target addresses for proposal calls + /// @param _values ETH values for proposal calls + /// @param _calldatas Function data for proposal calls + /// @param _description Description of the proposal + /// @param _proposalType Type of the proposal + /// @return proposalHash_ The hash of the submitted proposal function submitProposal( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, + uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external - returns (uint256 proposalId_) + returns (bytes32 proposalHash_) { _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - proposalId_ = ++_proposalCounter; + proposalHash_ = _hashProposal(_targets, _values, _calldatas, _description); + ProposalData storage proposal = _proposals[proposalHash_]; + + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } - ProposalData storage proposal = _proposals[proposalId_]; proposal.proposer = msg.sender; - proposal.targets = _targets; - proposal.values = _values; - proposal.calldatas = _calldatas; - proposal.description = _description; proposal.proposalType = _proposalType; + proposal.proposalTypeConfigurator = _proposalTypeConfigurator; proposal.inVoting = false; proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes - emit ProposalSubmitted(proposalId_, msg.sender, _targets, _values, _calldatas, _description, _proposalType); - - return proposalId_; + emit ProposalSubmitted( + proposalHash_, + msg.sender, + _targets, + _values, + _calldatas, + _description, + _proposalType, + _proposalTypeConfigurator + ); } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power). - /// @param _proposalId The ID of the proposal to approve. - function approveProposal(uint256 _proposalId) external { + /// @notice Approve a proposal (only callable by delegates with sufficient voting power) + /// @param _proposalHash The hash of the proposal to approve + function approveProposal(bytes32 _proposalHash) external { if (!canSignOff(msg.sender)) { revert ProposalValidator_InsufficientVotingPower(); } - ProposalData storage proposal = _proposals[_proposalId]; + ProposalData storage proposal = _proposals[_proposalHash]; if (proposal.delegateApprovals[msg.sender]) { revert ProposalValidator_ProposalAlreadyApproved(); @@ -260,40 +265,54 @@ contract ProposalValidator is Ownable { proposal.delegateApprovals[msg.sender] = true; proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted - emit ProposalApproved(_proposalId, msg.sender); + emit ProposalApproved(_proposalHash, msg.sender); } - /// @notice Move a proposal to voting phase after sufficient delegate approvals. - /// @dev After passing all checks, the proposal is submitted with a external call to the governor contract. - /// @param _proposalId The ID of the proposal to move to vote. - /// @return governorProposalId_ The proposal ID in the governor contract. - function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_) { - ProposalData storage proposal = _proposals[_proposalId]; + /// @notice Move a proposal to voting phase after sufficient delegate approvals + /// @param _targets Target addresses for proposal calls + /// @param _values ETH values for proposal calls + /// @param _calldatas Function data for proposal calls + /// @param _description Description of the proposal + /// @return governorProposalId_ The proposal ID in the governor contract + function moveToVote( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + external + returns (uint256 governorProposalId_) + { + // Verify that the provided data matches the proposalHash + bytes32 _proposalHash = _hashProposal(_targets, _values, _calldatas, _description); + + ProposalData storage proposal = _proposals[_proposalHash]; + + if (proposal.proposer == address(0)) { + revert ProposalValidator_ProposalDoesNotExist(); + } if (proposal.remainingApprovalsRequired > 0) { revert ProposalValidator_InsufficientApprovals(); } if (proposal.inVoting) { - revert ProposalValidator_ProposalAlreadyInVoting(); + revert ProposalValidator_ProposalAlreadySubmitted(); } proposal.inVoting = true; - governorProposalId_ = GOVERNOR.propose( - proposal.targets, proposal.values, proposal.calldatas, proposal.description, uint8(proposal.proposalType) - ); - - emit ProposalMovedToVote(_proposalId, msg.sender); + governorProposalId_ = + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposal.proposalTypeConfigurator); - return governorProposalId_; + emit ProposalMovedToVote(_proposalHash, msg.sender); } /// @notice Returns whether a delegate has enough voting power to approve a proposal. /// @param _delegate The address of the delegate to check. /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. function canSignOff(address _delegate) public view returns (bool canSignOff_) { - return VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; + canSignOff_ = VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; } /// @notice Sets the minimum voting power required for a delegate to approve proposals. @@ -371,7 +390,20 @@ contract ProposalValidator is Ownable { returns (bool isValid_) { (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); - return approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + } + + function _hashProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + internal + pure + returns (bytes32 proposalHash_) + { + return keccak256(abi.encode(_targets, _values, _calldatas, _description)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7e9c85a18f0..b58c5c7995b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -35,6 +35,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator public validator; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; + bytes32 public proposalHash; /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -52,9 +53,9 @@ contract ProposalValidator_Init is CommonTest { } /// @notice Helper function to make a (top) delegate approve a proposal. - function _approveProposal(address _delegate, uint256 _proposalId) internal { + function _approveProposal(address _delegate, bytes32 _proposalHash) internal { vm.prank(_delegate); - validator.approveProposal(_proposalId); + validator.approveProposal(_proposalHash); } function _getProposalTypesRequiredApprovalsAndImmutableData() @@ -181,13 +182,16 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Submit the proposal vm.prank(topDelegate_A); - uint256 proposalId = - validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); + bytes32 proposalHash = validator.submitProposal( + _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid + ); - assertEq(proposalId, 1); + assertEq(proposalHash, keccak256(abi.encode(_targets, _values, _calldatas, _description))); } } @@ -199,11 +203,14 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); + validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, invalidAttestationUid + ); } function test_submitProposal_wrongAttester_reverts() public { @@ -211,21 +218,22 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; // Create attestation with wrong delegate bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } } /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - uint256 proposalId; - function setUp() public override { super.setUp(); @@ -233,25 +241,26 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_approveProposal_succeeds() public { - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); } } /// @title ProposalValidator_ApproveProposal_TestFail /// @notice Sad path tests for approveProposal function contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { - uint256 proposalId; - function setUp() public override { super.setUp(); @@ -259,34 +268,37 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_approveProposal_insufficientVotingPower_reverts() public { vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalId); + _approveProposal(rando, proposalHash); } function test_approveProposal_alreadyApproved_reverts() public { - _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_A, proposalHash); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_A, proposalHash); } } /// @title ProposalValidator_MoveToVote_Test /// @notice Happy path tests for moveToVote function contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { - uint256 proposalId; address[] targets; uint256[] values; bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; + uint8 proposalTypeConfigurator; function setUp() public override { super.setUp(); @@ -294,26 +306,31 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); } function test_moveToVote_succeeds() public { _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(proposalId); + uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); assertEq(governorProposalId, 1); } @@ -322,12 +339,12 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { /// @title ProposalValidator_MoveToVote_TestFail /// @notice Sad path tests for moveToVote function contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { - uint256 proposalId; address[] targets; uint256[] values; bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; + uint8 proposalTypeConfigurator; function setUp() public override { super.setUp(); @@ -335,42 +352,47 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_moveToVote_insufficientApprovals_reverts() public { // Only approve with 3 delegates (need 4) - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } function test_moveToVote_alreadyProposed_reverts() public { // Approve with all 4 delegates - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } } @@ -401,41 +423,36 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - uint256 proposalId = - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - - assertEq(proposalId, 1); - - // It reverts when caller is not a top delegate - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalId); - - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - - // It reverts when proposal hasn't reached the required approvals - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(owner); - validator.moveToVote(proposalId); + bytes32 proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); - _approveProposal(topDelegate_D, proposalId); + // Collect all required approvals + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); + // Mock the governor call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); + // Move to vote phase vm.prank(owner); - validator.moveToVote(proposalId); + uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); // It reverts when proposal is already in voting phase - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } } From ed73cd0b3aa65b25442ce26183ff657d734e1816 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 23 May 2025 11:27:28 -0300 Subject: [PATCH 03/73] feat: add upgradeability to ProposalValidator contract (#384) * feat: add upgradeability to ProposalValidator contract * chore: fix styling * docs: use correct natspec in ProosalValidator contract * feat: add semver * feat: add reinitializable base --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 30 +- .../snapshots/abi/ApprovalVotingModule.json | 346 ++++++++++++++++++ .../snapshots/abi/ProposalValidator.json | 162 +++++--- .../snapshots/semver-lock.json | 4 + .../storageLayout/ApprovalVotingModule.json | 16 + .../storageLayout/ProposalValidator.json | 42 ++- .../src/governance/ProposalValidator.sol | 51 ++- .../test/governance/ProposalValidator.t.sol | 33 +- 8 files changed, 586 insertions(+), 98 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 4dc0cd45ad1..0034069fc25 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IGovernanceToken} from "./IGovernanceToken.sol"; -import {IOptimismGovernor} from "./IOptimismGovernor.sol"; +// Interfaces +import {IGovernanceToken} from './IGovernanceToken.sol'; +import {IOptimismGovernor} from './IOptimismGovernor.sol'; +import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. -interface IProposalValidator { +interface IProposalValidator is ISemver { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); + error ReinitializableBase_ZeroInitVersion(); struct ProposalData { address proposer; @@ -68,6 +71,8 @@ interface IProposalValidator { event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event Initialized(uint8 version); + function submitProposal( address[] memory _targets, uint256[] memory _values, @@ -113,18 +118,23 @@ interface IProposalValidator { function owner() external view returns (address); + function initVersion() external view returns (uint8); + function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - - function __constructor__( + + function initialize( address _owner, - IOptimismGovernor _governor, - IGovernanceToken _votingToken, - bytes32 _attestationSchemaUid, uint256 _minimumVotingPower, uint256 _votingCycleBlock, uint256 _distributionThreshold, - ProposalType[] memory _proposalTypes, + IProposalValidator.ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals, - ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + IProposalValidator.ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) external; + + function __constructor__( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _votingToken ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json new file mode 100644 index 00000000000..d531b81bb48 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json @@ -0,0 +1,346 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_governor", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "COUNTING_MODE", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "PROPOSAL_DATA_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "VOTE_PARAMS_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "budgetTokensSpent", + "type": "uint256" + } + ], + "name": "_afterExecute", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint8", + "name": "support", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "_countVote", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + } + ], + "name": "_formatExecuteParams", + "outputs": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "_voteSucceeded", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountTotalVotes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountVotes", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "proposals", + "outputs": [ + { + "internalType": "address", + "name": "governor", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initBalance", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "maxApprovals", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "criteria", + "type": "uint8" + }, + { + "internalType": "address", + "name": "budgetToken", + "type": "address" + }, + { + "internalType": "uint128", + "name": "criteriaValue", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "budgetAmount", + "type": "uint128" + } + ], + "internalType": "struct ProposalSettings", + "name": "settings", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "descriptionHash", + "type": "bytes32" + } + ], + "name": "propose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "BudgetExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "ExistingProposal", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParams", + "type": "error" + }, + { + "inputs": [], + "name": "MaxApprovalsExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxChoicesExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernor", + "type": "error" + }, + { + "inputs": [], + "name": "OptionsNotStrictlyAscending", + "type": "error" + }, + { + "inputs": [], + "name": "WrongProposalId", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index bdd01c8d1d9..0430ce901f4 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -2,9 +2,9 @@ { "inputs": [ { - "internalType": "address", - "name": "_owner", - "type": "address" + "internalType": "bytes32", + "name": "_attestationSchemaUid", + "type": "bytes32" }, { "internalType": "contract IOptimismGovernor", @@ -15,58 +15,6 @@ "internalType": "contract IGovernanceToken", "name": "_votingToken", "type": "address" - }, - { - "internalType": "bytes32", - "name": "_attestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_distributionThreshold", - "type": "uint256" - }, - { - "internalType": "enum ProposalValidator.ProposalType[]", - "name": "_proposalTypes", - "type": "uint8[]" - }, - { - "internalType": "uint256[]", - "name": "_requiredApprovals", - "type": "uint256[]" - }, - { - "components": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "string[]", - "name": "signatures", - "type": "string[]" - } - ], - "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", - "name": "_immutableProposalTypeDatas", - "type": "tuple[]" } ], "stateMutability": "nonpayable", @@ -156,6 +104,79 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + }, + { + "internalType": "enum ProposalValidator.ProposalType[]", + "name": "_proposalTypes", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "_requiredApprovals", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + } + ], + "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", + "name": "_immutableProposalTypeDatas", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "minimumVotingPower", @@ -342,6 +363,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "votingCycleBlock", @@ -368,6 +402,19 @@ "name": "DistributionThresholdSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -554,5 +601,10 @@ "inputs": [], "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 3801ba4bfac..45b69315a24 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -175,6 +175,10 @@ "initCodeHash": "0x2dfd6f7270eb35bb23ab79b81ac2c6c9550372e442f6952f7e3c7b025326110b", "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, + "src/governance/ProposalValidator.sol:ProposalValidator": { + "initCodeHash": "0x79a40d1aa2eca36a8a8bcacfed58e42b6eca856e2e12898433a88d8aeaa6e74d", + "sourceCodeHash": "0x0049245cc58386fd48e72280d4d629d520c6c21ab061379e8971fb14a58add8b" + }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", "sourceCodeHash": "0xf22c94ed20c32a8ed2705a22d12c6969c3c3bad409c4efe2f95b0db74f210e10" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json new file mode 100644 index 00000000000..43e2fd35e17 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "proposals", + "offset": 0, + "slot": "0", + "type": "mapping(uint256 => struct Proposal)" + }, + { + "bytes": "32", + "label": "accountVotesSet", + "offset": 0, + "slot": "1", + "type": "mapping(uint256 => mapping(address => struct EnumerableSetUpgradeable.UintSet))" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index f7fdeefae8c..9ffdaf07452 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -1,51 +1,79 @@ [ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "1600", + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "uint256[50]" + }, { "bytes": "20", "label": "_owner", "offset": 0, - "slot": "0", + "slot": "51", "type": "address" }, + { + "bytes": "1568", + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "uint256[49]" + }, { "bytes": "32", "label": "minimumVotingPower", "offset": 0, - "slot": "1", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycleBlock", "offset": 0, - "slot": "2", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "3", + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "_proposalRequiredApprovals", "offset": 0, - "slot": "4", + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, { "bytes": "32", "label": "_proposalTypeData", "offset": 0, - "slot": "5", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "6", + "slot": "106", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 6661bc17030..714526bf480 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Contracts -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -11,11 +12,13 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +/// @custom:proxied true /// @title ProposalValidator /// @notice The ProposalValidator contract is responsible for validating proposals and moving /// them to the vote phase on the Optimism Governor. -contract ProposalValidator is Ownable { +contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -165,34 +168,49 @@ contract ProposalValidator is Ownable { /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; - /// @notice Initializes the ProposalValidator contract. - /// @param _owner The address that will own the contract. + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + function version() public pure virtual returns (string memory) { + return "1.0.0-beta.1"; + } + + /// @notice Constructs the ProposalValidator contract. + /// @param _attestationSchemaUid The schema UID for attestations in EAS. /// @param _governor The Optimism Governor contract address. /// @param _votingToken The token used to determine voting power. - /// @param _attestationSchemaUid The schema UID for attestations in EAS. + constructor( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _votingToken + ) + ReinitializableBase(1) + { + ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + GOVERNOR = _governor; + VOTING_TOKEN = _votingToken; + _disableInitializers(); + } + + /// @notice Initializes the ProposalValidator contract. + /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _votingCycleBlock The block number of the current voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. - constructor( + function initialize( address _owner, - IOptimismGovernor _governor, - IGovernanceToken _votingToken, - bytes32 _attestationSchemaUid, uint256 _minimumVotingPower, uint256 _votingCycleBlock, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals, ImmutableProposalTypeData[] memory _immutableProposalTypeDatas - ) { - transferOwnership(_owner); - GOVERNOR = _governor; - VOTING_TOKEN = _votingToken; - ATTESTATION_SCHEMA_UID = _attestationSchemaUid; - + ) + external + reinitializer(initVersion()) + { _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleBlock(_votingCycleBlock); _setDistributionThreshold(_distributionThreshold); @@ -201,6 +219,9 @@ contract ProposalValidator is Ownable { _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; } + + __Ownable_init(); + transferOwnership(_owner); } /// @notice Submit a proposal for delegate approval diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b58c5c7995b..fe6169bcc0f 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -6,9 +6,11 @@ import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; +import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -33,6 +35,7 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_D; ProposalValidator public validator; + ProposalValidator public impl; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; @@ -110,17 +113,25 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData ) = _getProposalTypesRequiredApprovalsAndImmutableData(); - validator = new ProposalValidator( - owner, - governor, - governanceToken, - ATTESTATION_SCHEMA_UID, - MINIMUM_VOTING_POWER, - VOTING_CYCLE_BLOCK, - DISTRIBUTION_THRESHOLD, - proposalTypes, - requiredApprovals, - immutableProposalTypeData + impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); + + validator = ProposalValidator(address(new Proxy(owner))); + + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + VOTING_CYCLE_BLOCK, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals, + immutableProposalTypeData + ) + ) ); topDelegate_A = _makeTopDelegate("topDelegate_A"); From 49dd29021ad5bd8db438c45e51873581e5d7e62d Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 28 May 2025 01:56:04 -0300 Subject: [PATCH 04/73] fix: spec incosistencies (#398) * chore: remove votingCycleBlock variable * chore: remove ImmutableProposalTypeData --- .../governance/IProposalValidator.sol | 18 +---- .../snapshots/abi/ProposalValidator.json | 66 ------------------- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 20 +----- .../src/governance/ProposalValidator.sol | 41 +----------- .../test/governance/ProposalValidator.t.sol | 37 ++--------- 6 files changed, 14 insertions(+), 172 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 0034069fc25..da960c259b3 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -25,13 +25,7 @@ interface IProposalValidator is ISemver { mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; } - - struct ImmutableProposalTypeData { - address[] targets; - uint256[] values; - string[] signatures; - } - + enum ProposalType { ProtocolOrGovernorUpgrade, MaintenanceUpgrade, @@ -65,8 +59,6 @@ interface IProposalValidator is ISemver { event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - event VotingCycleBlockSet(uint256 newVotingCycleBlock); - event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); @@ -94,8 +86,6 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; - function setVotingCycleBlock(uint256 _votingCycleBlock) external; - function setDistributionThreshold(uint256 _distributionThreshold) external; function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; @@ -108,8 +98,6 @@ interface IProposalValidator is ISemver { function minimumVotingPower() external view returns (uint256); - function votingCycleBlock() external view returns (uint256); - function distributionThreshold() external view returns (uint256); function VOTING_TOKEN() external view returns (IGovernanceToken); @@ -125,11 +113,9 @@ interface IProposalValidator is ISemver { function initialize( address _owner, uint256 _minimumVotingPower, - uint256 _votingCycleBlock, uint256 _distributionThreshold, IProposalValidator.ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals, - IProposalValidator.ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + uint256[] memory _requiredApprovals ) external; function __constructor__( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 0430ce901f4..d22d4b2f105 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -129,11 +129,6 @@ "name": "_minimumVotingPower", "type": "uint256" }, - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - }, { "internalType": "uint256", "name": "_distributionThreshold", @@ -148,28 +143,6 @@ "internalType": "uint256[]", "name": "_requiredApprovals", "type": "uint256[]" - }, - { - "components": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "string[]", - "name": "signatures", - "type": "string[]" - } - ], - "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", - "name": "_immutableProposalTypeDatas", - "type": "tuple[]" } ], "name": "initialize", @@ -288,19 +261,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - } - ], - "name": "setVotingCycleBlock", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -376,19 +336,6 @@ "stateMutability": "pure", "type": "function" }, - { - "inputs": [], - "name": "votingCycleBlock", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "anonymous": false, "inputs": [ @@ -559,19 +506,6 @@ "name": "ProposalSubmitted", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newVotingCycleBlock", - "type": "uint256" - } - ], - "name": "VotingCycleBlockSet", - "type": "event" - }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 45b69315a24..503b5448b95 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x79a40d1aa2eca36a8a8bcacfed58e42b6eca856e2e12898433a88d8aeaa6e74d", - "sourceCodeHash": "0x0049245cc58386fd48e72280d4d629d520c6c21ab061379e8971fb14a58add8b" + "initCodeHash": "0x295082116c7ed6b02211af266697b6e0839987eba410c4654051ad62b76e308e", + "sourceCodeHash": "0xd47d9c8bfbb130a8f1290dab1b25c4f92da9f7d0e9a5b7923dd646178713593f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 9ffdaf07452..b531f8f69b0 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -41,39 +41,25 @@ "slot": "101", "type": "uint256" }, - { - "bytes": "32", - "label": "votingCycleBlock", - "offset": 0, - "slot": "102", - "type": "uint256" - }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "103", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "_proposalRequiredApprovals", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, - { - "bytes": "32", - "label": "_proposalTypeData", - "offset": 0, - "slot": "105", - "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" - }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "104", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 714526bf480..3512a160a4e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -61,16 +61,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 remainingApprovalsRequired; } - /// @notice Data structure for storing immutable proposal type data. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param signatures Function signatures for proposal calls. - struct ImmutableProposalTypeData { - address[] targets; - uint256[] values; - string[] signatures; - } - /*////////////////////////////////////////////////////////////// ENUMS //////////////////////////////////////////////////////////////*/ @@ -127,10 +117,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newMinimumVotingPower The new minimum voting power. event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - /// @notice Emitted when the voting cycle block is set. - /// @param newVotingCycleBlock The new voting cycle block. - event VotingCycleBlockSet(uint256 newVotingCycleBlock); - /// @notice Emitted when the distribution threshold is set. /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -153,18 +139,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The minimum voting power required for a delegate to approve proposals. uint256 public minimumVotingPower; - /// @notice The block number of the current voting cycle. - uint256 public votingCycleBlock; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; /// @notice The number of approvals required for each proposal type. mapping(ProposalType => uint256) private _proposalRequiredApprovals; - /// @notice The immutable data for each proposal type. - mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; - /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -194,30 +174,24 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. - /// @param _votingCycleBlock The block number of the current voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. - /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, - uint256 _votingCycleBlock, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals, - ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + uint256[] memory _requiredApprovals ) external reinitializer(initVersion()) { _setMinimumVotingPower(_minimumVotingPower); - _setVotingCycleBlock(_votingCycleBlock); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); - _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; } __Ownable_init(); @@ -342,12 +316,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setMinimumVotingPower(_minimumVotingPower); } - /// @notice Sets the block number of the current voting cycle. - /// @param _votingCycleBlock The new voting cycle block number. - function setVotingCycleBlock(uint256 _votingCycleBlock) external onlyOwner { - _setVotingCycleBlock(_votingCycleBlock); - } - /// @notice Sets the max amount of tokens that can be distributed in a proposal. /// @param _distributionThreshold The new distribution threshold. function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { @@ -434,13 +402,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit MinimumVotingPowerSet(_minimumVotingPower); } - /// @notice Private function to set the voting cycle block and emit event. - /// @param _votingCycleBlock The new voting cycle block number. - function _setVotingCycleBlock(uint256 _votingCycleBlock) private { - votingCycleBlock = _votingCycleBlock; - emit VotingCycleBlockSet(_votingCycleBlock); - } - /// @notice Private function to set the distribution threshold and emit event. /// @param _distributionThreshold The new distribution threshold. function _setDistributionThreshold(uint256 _distributionThreshold) private { diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fe6169bcc0f..52815bce0bb 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -22,7 +22,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP - uint256 public constant VOTING_CYCLE_BLOCK = 100; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; @@ -61,14 +60,10 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } - function _getProposalTypesRequiredApprovalsAndImmutableData() + function _getProposalTypesRequiredApprovals() internal pure - returns ( - ProposalValidator.ProposalType[] memory, - uint256[] memory, - ProposalValidator.ImmutableProposalTypeData[] memory - ) + returns (ProposalValidator.ProposalType[] memory, uint256[] memory) { ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -84,15 +79,7 @@ contract ProposalValidator_Init is CommonTest { requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; - ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData = - new ProposalValidator.ImmutableProposalTypeData[](5); - immutableProposalTypeData[0] = ProposalValidator.ImmutableProposalTypeData({ - targets: new address[](1), - values: new uint256[](1), - signatures: new string[](1) - }); - - return (proposalTypes, requiredApprovals, immutableProposalTypeData); + return (proposalTypes, requiredApprovals); } /// @dev Sets up the test suite. @@ -107,11 +94,8 @@ contract ProposalValidator_Init is CommonTest { "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false ); - ( - ProposalValidator.ProposalType[] memory proposalTypes, - uint256[] memory requiredApprovals, - ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData - ) = _getProposalTypesRequiredApprovalsAndImmutableData(); + (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = + _getProposalTypesRequiredApprovals(); impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); @@ -121,16 +105,7 @@ contract ProposalValidator_Init is CommonTest { IProxy(payable(address(validator))).upgradeToAndCall( address(impl), abi.encodeCall( - impl.initialize, - ( - owner, - MINIMUM_VOTING_POWER, - VOTING_CYCLE_BLOCK, - DISTRIBUTION_THRESHOLD, - proposalTypes, - requiredApprovals, - immutableProposalTypeData - ) + impl.initialize, (owner, MINIMUM_VOTING_POWER, DISTRIBUTION_THRESHOLD, proposalTypes, requiredApprovals) ) ); From ec4369c4714de27fa127f9d148ae7e6e5d17c6b2 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 28 May 2025 11:57:38 -0300 Subject: [PATCH 05/73] feat: admin functions (#393) * feat: add upgradeability to ProposalValidator contract * chore: fix styling * feat: add admin functions and tests * chore: more descriptive variables naming * test: expect event emissions * test: use fuzzing for setter functions * chore: run pre-pr * docs: use correct natspec in ProosalValidator contract * feat: add semver * feat: add reinitializable base * chore: run pre-pr * chore: run pre-pr --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 51 +++- .../snapshots/abi/ProposalValidator.json | 172 +++++++++++-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 11 +- .../src/governance/ProposalValidator.sol | 98 +++++++- .../test/governance/ProposalValidator.t.sol | 231 +++++++++++++++++- 6 files changed, 515 insertions(+), 52 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index da960c259b3..244044dfb6c 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -15,6 +15,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); + error ProposalValidator_VotingCycleAlreadySet(); error ReinitializableBase_ZeroInitVersion(); struct ProposalData { @@ -61,8 +62,15 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); - + event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + event VotingCycleDataSet( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); + event Initialized(uint8 version); function submitProposal( @@ -87,8 +95,27 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; - - function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) external; + + function initialize( + address _owner, + uint256 _minimumVotingPower, + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals + ) external; function renounceOwnership() external; @@ -109,14 +136,14 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - - function initialize( - address _owner, - uint256 _minimumVotingPower, - uint256 _distributionThreshold, - IProposalValidator.ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals - ) external; + + function proposalRequiredApprovals(ProposalType) external view returns (uint256); + + function votingCycles(uint256) external view returns ( + uint256 startingBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); function __constructor__( bytes32 _attestationSchemaUid, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index d22d4b2f105..efe0ba90eef 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -129,6 +129,26 @@ "name": "_minimumVotingPower", "type": "uint256" }, + { + "internalType": "uint256", + "name": "_cycleNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleDistributionLimit", + "type": "uint256" + }, { "internalType": "uint256", "name": "_distributionThreshold", @@ -210,6 +230,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "", + "type": "uint8" + } + ], + "name": "proposalRequiredApprovals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "renounceOwnership", @@ -256,7 +295,35 @@ "type": "uint256" } ], - "name": "setProposalRequiredApprovals", + "name": "setProposalTypeApprovalThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_cycleNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleDistributionLimit", + "type": "uint256" + } + ], + "name": "setVotingCycleData", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -336,6 +403,35 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "votingCycles", + "outputs": [ + { + "internalType": "uint256", + "name": "startingBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "votingCycleDistributionLimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "anonymous": false, "inputs": [ @@ -394,25 +490,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ProposalValidator.ProposalType", - "name": "proposalType", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "newApprovalThreshold", - "type": "uint256" - } - ], - "name": "ProposalApprovalThresholdSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -506,6 +583,56 @@ "name": "ProposalSubmitted", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newApprovalThreshold", + "type": "uint256" + } + ], + "name": "ProposalTypeApprovalThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cycleNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "votingCycleDistributionLimit", + "type": "uint256" + } + ], + "name": "VotingCycleDataSet", + "type": "event" + }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", @@ -536,6 +663,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_VotingCycleAlreadySet", + "type": "error" + }, { "inputs": [], "name": "ReinitializableBase_ZeroInitVersion", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 503b5448b95..3f214f26cd0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x295082116c7ed6b02211af266697b6e0839987eba410c4654051ad62b76e308e", - "sourceCodeHash": "0xd47d9c8bfbb130a8f1290dab1b25c4f92da9f7d0e9a5b7923dd646178713593f" + "initCodeHash": "0x218b5001f56c04cefe63991cdfa08d79595e94ba203dd615818f3e5c570d9f26", + "sourceCodeHash": "0xd9cbe54c1f1c0152ee3da444f39f4cf6b0f6087d6b8c3da18ed8beb830835029" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index b531f8f69b0..cf455740bef 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -50,16 +50,23 @@ }, { "bytes": "32", - "label": "_proposalRequiredApprovals", + "label": "votingCycles", "offset": 0, "slot": "103", + "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" + }, + { + "bytes": "32", + "label": "proposalRequiredApprovals", + "offset": 0, + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "104", + "slot": "105", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3512a160a4e..26d1b5e8add 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -38,6 +38,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); + /// @notice Thrown when a voting cycle is already set. + error ProposalValidator_VotingCycleAlreadySet(); + /// @notice Thrown when a proposal does not exist. error ProposalValidator_ProposalDoesNotExist(); @@ -61,6 +64,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 remainingApprovalsRequired; } + /// @notice Data structure for storing voting cycle data. + /// @param startingBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. + struct VotingCycleData { + uint256 startingBlock; + uint256 duration; + uint256 votingCycleDistributionLimit; + } + /*////////////////////////////////////////////////////////////// ENUMS //////////////////////////////////////////////////////////////*/ @@ -117,6 +130,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newMinimumVotingPower The new minimum voting power. event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + /// @notice Emitted when the voting cycle data is set. + /// @param cycleNumber The number of the voting cycle. + /// @param startBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + /// @notice Emitted when the distribution threshold is set. /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -124,7 +146,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the number of approvals required for a proposal type is set. /// @param proposalType The type of proposal. /// @param newApprovalThreshold The new approval threshold. - event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -142,8 +164,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; + /// @notice Mapping of voting cycle numbers to their corresponding data. + mapping(uint256 => VotingCycleData) public votingCycles; + /// @notice The number of approvals required for each proposal type. - mapping(ProposalType => uint256) private _proposalRequiredApprovals; + mapping(ProposalType => uint256) public proposalRequiredApprovals; /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -174,12 +199,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. + /// @param _cycleNumber The number of the current voting cycle. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals @@ -188,10 +221,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { reinitializer(initVersion()) { _setMinimumVotingPower(_minimumVotingPower); + _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { - _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); + _setProposalTypeApprovalThreshold(_proposalTypes[i], _requiredApprovals[i]); } __Ownable_init(); @@ -316,6 +350,23 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setMinimumVotingPower(_minimumVotingPower); } + /// @notice Sets the data of a voting cycle. + /// @param _cycleNumber The number of the voting cycle to set. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) + external + onlyOwner + { + _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + } + /// @notice Sets the max amount of tokens that can be distributed in a proposal. /// @param _distributionThreshold The new distribution threshold. function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { @@ -325,8 +376,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Sets the number of approvals required for each proposal type. /// @param _proposalType The type of proposal to set the required approvals for. /// @param _requiredApprovals The new required approvals. - function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external onlyOwner { - _setProposalRequiredApprovals(_proposalType, _requiredApprovals); + function setProposalTypeApprovalThreshold( + ProposalType _proposalType, + uint256 _requiredApprovals + ) + external + onlyOwner + { + _setProposalTypeApprovalThreshold(_proposalType, _requiredApprovals); } /// @notice Validates a proposal before submission. @@ -402,6 +459,31 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit MinimumVotingPowerSet(_minimumVotingPower); } + /// @notice Private function to set the voting cycle data and emit event. + /// @param _cycleNumber The number of the voting cycle to set. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + function _setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) + private + { + if (votingCycles[_cycleNumber].startingBlock != 0) { + revert ProposalValidator_VotingCycleAlreadySet(); + } + + votingCycles[_cycleNumber] = VotingCycleData({ + startingBlock: _startBlock, + duration: _duration, + votingCycleDistributionLimit: _votingCycleDistributionLimit + }); + emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + } + /// @notice Private function to set the distribution threshold and emit event. /// @param _distributionThreshold The new distribution threshold. function _setDistributionThreshold(uint256 _distributionThreshold) private { @@ -412,8 +494,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Private function to set a proposal's type required approvals and emit event. /// @param _proposalType The type of proposal to set the required approvals for. /// @param _requiredApprovals The new required approvals. - function _setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) private { - _proposalRequiredApprovals[_proposalType] = _requiredApprovals; - emit ProposalApprovalThresholdSet(_proposalType, _requiredApprovals); + function _setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) private { + proposalRequiredApprovals[_proposalType] = _requiredApprovals; + emit ProposalTypeApprovalThresholdSet(_proposalType, _requiredApprovals); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 52815bce0bb..295781bbdd5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Interfaces import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; @@ -18,10 +19,39 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; +/// @title ProposalValidatorForTest +/// @notice A test contract that exposes the private _hashProposal function +contract ProposalValidatorForTest is ProposalValidator { + constructor( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _governanceToken + ) + ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) + { } + + function hashProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + public + pure + returns (bytes32) + { + return _hashProposal(_targets, _values, _calldatas, _description); + } +} + /// @title ProposalValidator_Init /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP + uint256 public constant CYCLE_NUMBER = 1; + uint256 public constant START_BLOCK = 1000000; + uint256 public constant DURATION = 100; + uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; @@ -33,12 +63,31 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_C; address topDelegate_D; - ProposalValidator public validator; - ProposalValidator public impl; + ProposalValidatorForTest public validator; + ProposalValidatorForTest public impl; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalValidator.ProposalType proposalType, + uint8 proposalTypeConfigurator + ); + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalTypeApprovalThresholdSet(ProposalValidator.ProposalType proposalType, uint256 newApprovalThreshold); + /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { vm.mockCall(_receiver, _calldata, _returned); @@ -97,15 +146,26 @@ contract ProposalValidator_Init is CommonTest { (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = _getProposalTypesRequiredApprovals(); - impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); - validator = ProposalValidator(address(new Proxy(owner))); + validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), abi.encodeCall( - impl.initialize, (owner, MINIMUM_VOTING_POWER, DISTRIBUTION_THRESHOLD, proposalTypes, requiredApprovals) + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals + ) ) ); @@ -170,6 +230,20 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); + + // Expect event to be emitted + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedProposalHash, + topDelegate_A, + _targets, + _values, + _calldatas, + _description, + proposalType, + proposalTypeConfigurator + ); // Submit the proposal vm.prank(topDelegate_A); @@ -177,7 +251,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid ); - assertEq(proposalHash, keccak256(abi.encode(_targets, _values, _calldatas, _description))); + assertEq(proposalHash, expectedProposalHash); } } @@ -237,9 +311,24 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { } function test_approveProposal_succeeds() public { + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_A); _approveProposal(topDelegate_A, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_B); _approveProposal(topDelegate_B, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_C); _approveProposal(topDelegate_C, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_D); _approveProposal(topDelegate_D, proposalHash); } } @@ -315,6 +404,10 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { abi.encode(1) ); + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(proposalHash, owner); + vm.prank(owner); uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); @@ -397,7 +490,100 @@ contract ProposalValidator_Getters_Test is ProposalValidator_Init { /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { -// TODO: Implement tests for setters + function testFuzz_setMinimumVotingPower_succeeds(uint256 newMinimumVotingPower) public { + // Expect the MinimumVotingPowerSet event to be emitted + vm.expectEmit(address(validator)); + emit MinimumVotingPowerSet(newMinimumVotingPower); + + vm.prank(owner); + validator.setMinimumVotingPower(newMinimumVotingPower); + + assertEq(validator.minimumVotingPower(), newMinimumVotingPower); + } + + function test_setMinimumVotingPower_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setMinimumVotingPower(10000 ether); + } + + function testFuzz_setVotingCycleData_succeeds( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle + + // Expect the VotingCycleDataSet event to be emitted + vm.expectEmit(address(validator)); + emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); + + vm.prank(owner); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + + (uint256 actualStartBlock, uint256 actualDuration, uint256 actualDistributionLimit) = + validator.votingCycles(cycleNumber); + + assertEq(actualStartBlock, startBlock); + assertEq(actualDuration, duration); + assertEq(actualDistributionLimit, distributionLimit); + } + + function test_setVotingCycleData_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + } + + function test_setVotingCycleData_votingCycleAlreadySet_reverts() public { + vm.prank(owner); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + + vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); + vm.prank(owner); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + } + + function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { + // Expect the DistributionThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit DistributionThresholdSet(newDistributionThreshold); + + vm.prank(owner); + validator.setDistributionThreshold(newDistributionThreshold); + + assertEq(validator.distributionThreshold(), newDistributionThreshold); + } + + function test_setDistributionThreshold_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setDistributionThreshold(10000 ether); + } + + function testFuzz_setProposalTypeApprovalThreshold_succeeds(uint8 proposalTypeValue, uint256 newThreshold) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Expect the ProposalTypeApprovalThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit ProposalTypeApprovalThresholdSet(proposalType, newThreshold); + + vm.prank(owner); + validator.setProposalTypeApprovalThreshold(proposalType, newThreshold); + + assertEq(validator.proposalRequiredApprovals(proposalType), newThreshold); + } + + function test_setProposalTypeApprovalThreshold_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setProposalTypeApprovalThreshold(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, 4); + } } /// @title ProposalValidator_Integration_Test @@ -412,15 +598,40 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Expect ProposalSubmitted event + bytes32 expectedProposalHash = keccak256(abi.encode(targets, values, calldatas, description)); + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedProposalHash, + topDelegate_A, + targets, + values, + calldatas, + description, + proposalType, + proposalTypeConfigurator + ); + vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitProposal( targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid ); - // Collect all required approvals + // Expect ProposalApproved events for each approval + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_A); _approveProposal(topDelegate_A, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_B); _approveProposal(topDelegate_B, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_C); _approveProposal(topDelegate_C, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_D); _approveProposal(topDelegate_D, proposalHash); // Mock the governor call @@ -432,6 +643,10 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { abi.encode(1) ); + // Expect ProposalMovedToVote event + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(proposalHash, owner); + // Move to vote phase vm.prank(owner); uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); From 726f2bdfe642f1419618a740ea4d862bc83da4f5 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:03:12 -0300 Subject: [PATCH 06/73] refactor: proposal type data struct (#401) * refactor: use proposalTypesData mapping * docs: improve natspec * feat: add mismatched lenghts check in contract initializer * fix: emit ProposalTypeDataSet with missing arg * refactor: correct naming in test helper functions * refactor: correct variable naming in fuzz tests * perf: remove redundant fields from ProposalData struct * chore: run pre-pr * chore: improve code formatting --- .../governance/IProposalValidator.sol | 21 +- .../snapshots/abi/ProposalValidator.json | 71 ++++-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 4 +- .../src/governance/ProposalValidator.sol | 84 ++++--- .../test/governance/ProposalValidator.t.sol | 237 +++++++++++++----- 6 files changed, 299 insertions(+), 122 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 244044dfb6c..00481da61ec 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -16,15 +16,20 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_VotingCycleAlreadySet(); + error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); struct ProposalData { address proposer; ProposalType proposalType; - uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; - uint256 remainingApprovalsRequired; + uint256 approvalCount; + } + + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalTypeConfigurator; } enum ProposalType { @@ -62,7 +67,7 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); event VotingCycleDataSet( uint256 cycleNumber, @@ -79,7 +84,6 @@ interface IProposalValidator is ISemver { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, - uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external returns (bytes32 proposalHash_); @@ -96,7 +100,10 @@ interface IProposalValidator is ISemver { function setDistributionThreshold(uint256 _distributionThreshold) external; - function setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) external; + function setProposalTypeData( + ProposalType _proposalType, + ProposalTypeData memory _proposalTypeData + ) external; function setVotingCycleData( uint256 _cycleNumber, @@ -114,7 +121,7 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals + ProposalTypeData[] memory _proposalTypesData ) external; function renounceOwnership() external; @@ -137,7 +144,7 @@ interface IProposalValidator is ISemver { function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function proposalRequiredApprovals(ProposalType) external view returns (uint256); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalTypeConfigurator); function votingCycles(uint256) external view returns ( uint256 startingBlock, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index efe0ba90eef..edc205a5ad5 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -160,9 +160,21 @@ "type": "uint8[]" }, { - "internalType": "uint256[]", - "name": "_requiredApprovals", - "type": "uint256[]" + "components": [ + { + "internalType": "uint256", + "name": "requiredApprovals", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" + } + ], + "internalType": "struct ProposalValidator.ProposalTypeData[]", + "name": "_proposalTypesData", + "type": "tuple[]" } ], "name": "initialize", @@ -238,12 +250,17 @@ "type": "uint8" } ], - "name": "proposalRequiredApprovals", + "name": "proposalTypesData", "outputs": [ { "internalType": "uint256", - "name": "", + "name": "requiredApprovals", "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], "stateMutability": "view", @@ -290,12 +307,24 @@ "type": "uint8" }, { - "internalType": "uint256", - "name": "_requiredApprovals", - "type": "uint256" - } - ], - "name": "setProposalTypeApprovalThreshold", + "components": [ + { + "internalType": "uint256", + "name": "requiredApprovals", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" + } + ], + "internalType": "struct ProposalValidator.ProposalTypeData", + "name": "_proposalTypeData", + "type": "tuple" + } + ], + "name": "setProposalTypeData", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -355,11 +384,6 @@ "name": "_proposalType", "type": "uint8" }, - { - "internalType": "uint8", - "name": "_proposalTypeConfigurator", - "type": "uint8" - }, { "internalType": "bytes32", "name": "_attestationUid", @@ -595,11 +619,17 @@ { "indexed": false, "internalType": "uint256", - "name": "newApprovalThreshold", + "name": "requiredApprovals", "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], - "name": "ProposalTypeApprovalThresholdSet", + "name": "ProposalTypeDataSet", "type": "event" }, { @@ -663,6 +693,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalTypesDataLengthMismatch", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_VotingCycleAlreadySet", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 3f214f26cd0..a2d4ea59ac6 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x218b5001f56c04cefe63991cdfa08d79595e94ba203dd615818f3e5c570d9f26", - "sourceCodeHash": "0xd9cbe54c1f1c0152ee3da444f39f4cf6b0f6087d6b8c3da18ed8beb830835029" + "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", + "sourceCodeHash": "0xd7d94a765bec0d80cac4bce4e62270a27a830b103dd554d7a4be540a49f8f4d5" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index cf455740bef..8b940980418 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -57,10 +57,10 @@ }, { "bytes": "32", - "label": "proposalRequiredApprovals", + "label": "proposalTypesData", "offset": 0, "slot": "104", - "type": "mapping(enum ProposalValidator.ProposalType => uint256)" + "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 26d1b5e8add..c2b4ab06328 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -44,27 +44,36 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when a proposal does not exist. error ProposalValidator_ProposalDoesNotExist(); + /// @notice Thrown when the length of the proposal types and proposal types data arrays do not match. + error ProposalValidator_ProposalTypesDataLengthMismatch(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ - /// @notice Data structure for storing proposal information. + /// @notice Struct for storing proposal information. /// @param proposer The address that submitted the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. - /// @param proposalTypeConfigurator Configuration value specific to the proposal type. /// @param inVoting Whether the proposal has been moved to the voting phase. /// @param delegateApprovals Mapping of delegate addresses to their approval status. - /// @param remainingApprovalsRequired Number of approvals still needed before voting. + /// @param approvalCount Number of approvals received so far. struct ProposalData { address proposer; ProposalType proposalType; - uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; - uint256 remainingApprovalsRequired; + uint256 approvalCount; + } + + /// @notice Struct for storing explicit data for each proposal type. + /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for voting. + /// @param proposalTypeConfigurator The voting module each proposal type must use. + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalTypeConfigurator; } - /// @notice Data structure for storing voting cycle data. + /// @notice Struct for storing voting cycle data. /// @param startingBlock The block number of the starting block of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. @@ -143,10 +152,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); - /// @notice Emitted when the number of approvals required for a proposal type is set. + /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. - /// @param newApprovalThreshold The new approval threshold. - event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + /// @param requiredApprovals The required number of approvals. + /// @param proposalTypeConfigurator The proposal type configurator. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -167,8 +177,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of voting cycle numbers to their corresponding data. mapping(uint256 => VotingCycleData) public votingCycles; - /// @notice The number of approvals required for each proposal type. - mapping(ProposalType => uint256) public proposalRequiredApprovals; + /// @notice Mapping of proposal types to their corresponding data. + mapping(ProposalType => ProposalTypeData) public proposalTypesData; /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -204,8 +214,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. - /// @param _proposalTypes Array of proposal types to set approval thresholds for. - /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. + /// @param _proposalTypes Array of proposal types to set data for. + /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, @@ -215,17 +225,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals + ProposalTypeData[] memory _proposalTypesData ) external reinitializer(initVersion()) { + if (_proposalTypes.length != _proposalTypesData.length) { + revert ProposalValidator_ProposalTypesDataLengthMismatch(); + } + _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { - _setProposalTypeApprovalThreshold(_proposalTypes[i], _requiredApprovals[i]); + _setProposalTypeData(_proposalTypes[i], _proposalTypesData[i]); } __Ownable_init(); @@ -245,7 +259,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, - uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external @@ -260,11 +273,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadySubmitted(); } + ProposalTypeData memory proposalTypeData = proposalTypesData[_proposalType]; + proposal.proposer = msg.sender; proposal.proposalType = _proposalType; - proposal.proposalTypeConfigurator = _proposalTypeConfigurator; proposal.inVoting = false; - proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes emit ProposalSubmitted( proposalHash_, @@ -274,7 +287,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _calldatas, _description, _proposalType, - _proposalTypeConfigurator + proposalTypeData.proposalTypeConfigurator ); } @@ -292,7 +305,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposal.delegateApprovals[msg.sender] = true; - proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted + proposal.approvalCount++; emit ProposalApproved(_proposalHash, msg.sender); } @@ -321,7 +334,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalDoesNotExist(); } - if (proposal.remainingApprovalsRequired > 0) { + ProposalTypeData memory proposalTypeData = proposalTypesData[proposal.proposalType]; + if (proposal.approvalCount < proposalTypeData.requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } @@ -332,7 +346,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposal.proposalTypeConfigurator); + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalTypeConfigurator); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -373,17 +387,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setDistributionThreshold(_distributionThreshold); } - /// @notice Sets the number of approvals required for each proposal type. - /// @param _proposalType The type of proposal to set the required approvals for. - /// @param _requiredApprovals The new required approvals. - function setProposalTypeApprovalThreshold( + /// @notice Sets the data for a proposal type. + /// @param _proposalType The type of proposal to set the data for. + /// @param _proposalTypeData The data for the proposal type. + function setProposalTypeData( ProposalType _proposalType, - uint256 _requiredApprovals + ProposalTypeData memory _proposalTypeData ) external onlyOwner { - _setProposalTypeApprovalThreshold(_proposalType, _requiredApprovals); + _setProposalTypeData(_proposalType, _proposalTypeData); } /// @notice Validates a proposal before submission. @@ -491,11 +505,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit DistributionThresholdSet(_distributionThreshold); } - /// @notice Private function to set a proposal's type required approvals and emit event. - /// @param _proposalType The type of proposal to set the required approvals for. - /// @param _requiredApprovals The new required approvals. - function _setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) private { - proposalRequiredApprovals[_proposalType] = _requiredApprovals; - emit ProposalTypeApprovalThresholdSet(_proposalType, _requiredApprovals); + /// @notice Private function to set a proposal's type data. + /// @param _proposalType The type of proposal to set the data for. + /// @param _proposalTypeData The data for the proposal type. + function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { + proposalTypesData[_proposalType] = _proposalTypeData; + emit ProposalTypeDataSet( + _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalTypeConfigurator + ); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 295781bbdd5..b0c16baf572 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -86,7 +86,9 @@ contract ProposalValidator_Init is CommonTest { uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit ); event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeApprovalThresholdSet(ProposalValidator.ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeDataSet( + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator + ); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -109,10 +111,10 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } - function _getProposalTypesRequiredApprovals() + function _getProposalTypesAndData() internal pure - returns (ProposalValidator.ProposalType[] memory, uint256[] memory) + returns (ProposalValidator.ProposalType[] memory, ProposalValidator.ProposalTypeData[] memory) { ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -121,30 +123,37 @@ contract ProposalValidator_Init is CommonTest { proposalTypes[3] = ProposalValidator.ProposalType.GovernanceFund; proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; - uint256[] memory requiredApprovals = new uint256[](5); - requiredApprovals[0] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[1] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[2] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; - - return (proposalTypes, requiredApprovals); - } - - /// @dev Sets up the test suite. - function setUp() public virtual override { - super.setUp(); - owner = governanceToken.owner(); - rando = makeAddr("rando"); - governor = IOptimismGovernor(makeAddr("governor")); - - vm.prank(owner); - ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false - ); - - (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = - _getProposalTypesRequiredApprovals(); + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[2] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[3] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[4] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + + return (proposalTypes, proposalTypesData); + } + + /// @notice Initializes the validator + function _initializeValidator() internal virtual { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); @@ -164,10 +173,25 @@ contract ProposalValidator_Init is CommonTest { DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, proposalTypes, - requiredApprovals + proposalTypesData ) ) ); + } + + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + owner = governanceToken.owner(); + rando = makeAddr("rando"); + governor = IOptimismGovernor(makeAddr("governor")); + + vm.prank(owner); + ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + ); + + _initializeValidator(); topDelegate_A = _makeTopDelegate("topDelegate_A"); topDelegate_B = _makeTopDelegate("topDelegate_B"); @@ -247,9 +271,8 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { // Submit the proposal vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitProposal( - _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid - ); + bytes32 proposalHash = + validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); assertEq(proposalHash, expectedProposalHash); } @@ -268,9 +291,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, invalidAttestationUid - ); + validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); } function test_submitProposal_wrongAttester_reverts() public { @@ -285,9 +306,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } } @@ -305,9 +324,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_approveProposal_succeeds() public { @@ -347,9 +364,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_approveProposal_insufficientVotingPower_reverts() public { @@ -385,9 +400,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); _approveProposal(topDelegate_A, proposalHash); _approveProposal(topDelegate_B, proposalHash); @@ -435,9 +448,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_moveToVote_insufficientApprovals_reverts() public { @@ -564,25 +575,41 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { validator.setDistributionThreshold(10000 ether); } - function testFuzz_setProposalTypeApprovalThreshold_succeeds(uint8 proposalTypeValue, uint256 newThreshold) public { + function testFuzz_setProposalTypeData_succeeds( + uint8 proposalTypeValue, + uint256 newRequiredApprovals, + uint8 newConfigurator + ) + public + { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Expect the ProposalTypeApprovalThresholdSet event to be emitted + ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ + requiredApprovals: newRequiredApprovals, + proposalTypeConfigurator: newConfigurator + }); + + // Expect the ProposalTypeDataSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalTypeApprovalThresholdSet(proposalType, newThreshold); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newConfigurator); vm.prank(owner); - validator.setProposalTypeApprovalThreshold(proposalType, newThreshold); + validator.setProposalTypeData(proposalType, newData); - assertEq(validator.proposalRequiredApprovals(proposalType), newThreshold); + (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalType); + assertEq(requiredApprovals, newRequiredApprovals); + assertEq(proposalTypeConfigurator, newConfigurator); } - function test_setProposalTypeApprovalThreshold_notOwner_reverts() public { + function test_setProposalTypeData_notOwner_reverts() public { + ProposalValidator.ProposalTypeData memory newData = + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalTypeConfigurator: 0 }); + vm.prank(rando); vm.expectRevert("Ownable: caller is not the owner"); - validator.setProposalTypeApprovalThreshold(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, 4); + validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } } @@ -613,9 +640,8 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { ); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + bytes32 proposalHash = + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); // Expect ProposalApproved events for each approval vm.expectEmit(address(validator)); @@ -657,3 +683,96 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { validator.moveToVote(targets, values, calldatas, description); } } + +/// @title ProposalValidator_Initialize_Test +/// @notice Tests for the initialize function +contract ProposalValidator_Initialize_Test is ProposalValidator_Init { + /// @dev Override to create validator proxy without initialization for testing + function _initializeValidator() internal override { + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + validator = ProposalValidatorForTest(address(new Proxy(owner))); + // Initialize will be tested manually + } + + function test_initialize_succeeds() public { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); + + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); + + // Verify initialization was successful + assertEq(validator.minimumVotingPower(), MINIMUM_VOTING_POWER); + assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.owner(), owner); + + // Verify voting cycle data + (uint256 startBlock, uint256 duration, uint256 distributionLimit) = validator.votingCycles(CYCLE_NUMBER); + assertEq(startBlock, START_BLOCK); + assertEq(duration, DURATION); + assertEq(distributionLimit, DISTRIBUTION_LIMIT); + + // Verify proposal type data + for (uint256 i = 0; i < proposalTypes.length; i++) { + (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalTypes[i]); + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + assertEq(proposalTypeConfigurator, 0); + } + } + + function test_initialize_mismatchedArrayLengths_reverts() public { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + + // Create mismatched array with different length + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + + vm.prank(owner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); + } +} From 9962ae52861bcd6832c8d69656a6d8162a0d2a42 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:37:55 -0300 Subject: [PATCH 07/73] refactor: voting module naming (#404) * refactor: rename proposalTypeConfigurator to proposalVotingModule * chore: run pre-pr * fix: missing rename in comment * chore: run pre-pr * docs: fix natspec description --- .../governance/IProposalValidator.sol | 8 +-- .../snapshots/abi/ProposalValidator.json | 10 +-- .../snapshots/semver-lock.json | 2 +- .../src/governance/ProposalValidator.sol | 21 +++--- .../test/governance/ProposalValidator.t.sol | 66 +++++++++---------- 5 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 00481da61ec..7ab9b4194a5 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -29,7 +29,7 @@ interface IProposalValidator is ISemver { struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; } enum ProposalType { @@ -48,7 +48,7 @@ interface IProposalValidator is ISemver { bytes[] calldatas, string description, ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); event ProposalApproved( @@ -67,7 +67,7 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); event VotingCycleDataSet( uint256 cycleNumber, @@ -144,7 +144,7 @@ interface IProposalValidator is ISemver { function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalTypeConfigurator); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( uint256 startingBlock, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index edc205a5ad5..acb1665cd84 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -168,7 +168,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -259,7 +259,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -315,7 +315,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -600,7 +600,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -625,7 +625,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index a2d4ea59ac6..823c10c7b40 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -177,7 +177,7 @@ }, "src/governance/ProposalValidator.sol:ProposalValidator": { "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", - "sourceCodeHash": "0xd7d94a765bec0d80cac4bce4e62270a27a830b103dd554d7a4be540a49f8f4d5" + "sourceCodeHash": "0xddf3e6506f0155d0120e467cb1437c49b1eff28d362b8e7de55101cd91e43427" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c2b4ab06328..dcb729480be 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -66,11 +66,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Struct for storing explicit data for each proposal type. - /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for voting. - /// @param proposalTypeConfigurator The voting module each proposal type must use. + /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for + /// voting. + /// @param proposalVotingModule The voting module each proposal type must use. struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; } /// @notice Struct for storing voting cycle data. @@ -113,7 +114,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param calldatas Function data for proposal calls. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. - /// @param proposalTypeConfigurator Configuration value specific to the proposal type. + /// @param proposalVotingModule Voting module specific to the proposal type. event ProposalSubmitted( bytes32 indexed proposalHash, address indexed proposer, @@ -122,7 +123,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes[] calldatas, string description, ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); /// @notice Emitted when a delegate approves a proposal. @@ -155,8 +156,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalTypeConfigurator The proposal type configurator. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); + /// @param proposalVotingModule The proposal voting module. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -287,7 +288,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _calldatas, _description, _proposalType, - proposalTypeData.proposalTypeConfigurator + proposalTypeData.proposalVotingModule ); } @@ -346,7 +347,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalTypeConfigurator); + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalVotingModule); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -511,7 +512,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { proposalTypesData[_proposalType] = _proposalTypeData; emit ProposalTypeDataSet( - _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalTypeConfigurator + _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalVotingModule ); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b0c16baf572..1c6745438ad 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -77,7 +77,7 @@ contract ProposalValidator_Init is CommonTest { bytes[] calldatas, string description, ProposalValidator.ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); @@ -87,7 +87,7 @@ contract ProposalValidator_Init is CommonTest { ); event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalTypeDataSet( - ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); /// @notice Helper function to setup a mock and expect a call to it. @@ -126,23 +126,23 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); return (proposalTypes, proposalTypesData); @@ -252,7 +252,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); @@ -266,7 +266,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _calldatas, _description, proposalType, - proposalTypeConfigurator + proposalVotingModule ); // Submit the proposal @@ -286,7 +286,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID vm.prank(topDelegate_A); @@ -299,7 +299,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; // Create attestation with wrong delegate bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); @@ -320,7 +320,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -360,7 +360,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -388,7 +388,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; function setUp() public override { super.setUp(); @@ -396,7 +396,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypeConfigurator = 0; + proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -411,9 +411,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { function test_moveToVote_succeeds() public { _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -436,7 +434,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; function setUp() public override { super.setUp(); @@ -444,7 +442,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypeConfigurator = 0; + proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -471,9 +469,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -588,7 +584,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalTypeConfigurator: newConfigurator + proposalVotingModule: newConfigurator }); // Expect the ProposalTypeDataSet event to be emitted @@ -598,14 +594,14 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setProposalTypeData(proposalType, newData); - (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalType); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalTypeConfigurator, newConfigurator); + assertEq(proposalVotingModule, newConfigurator); } function test_setProposalTypeData_notOwner_reverts() public { ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalTypeConfigurator: 0 }); + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); vm.prank(rando); vm.expectRevert("Ownable: caller is not the owner"); @@ -622,7 +618,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); // Expect ProposalSubmitted event @@ -636,7 +632,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { calldatas, description, proposalType, - proposalTypeConfigurator + proposalVotingModule ); vm.prank(topDelegate_A); @@ -663,9 +659,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { // Mock the governor call _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -732,9 +726,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalTypes[i]); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalTypeConfigurator, 0); + assertEq(proposalVotingModule, 0); } } @@ -748,11 +742,11 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); vm.prank(owner); From dd493da26029bfff418cf5a52a6ef3d2d1aa0c46 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:13:25 -0300 Subject: [PATCH 08/73] feat: add hashProposalWithModule function (#403) * feat: add hashProposalWithModule function * chore: run pre-pr * test: add hashProposalWithModule tests * refactor: remove hashProposal function * chore: run pre-pr --- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 24 +++++---- .../test/governance/ProposalValidator.t.sol | 51 ++++++++++++++++--- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 823c10c7b40..e769c025436 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", - "sourceCodeHash": "0xddf3e6506f0155d0120e467cb1437c49b1eff28d362b8e7de55101cd91e43427" + "initCodeHash": "0x553e9a5abda992f985f23d02f07c169be7ee39063d6dbd00742b4298089d3602", + "sourceCodeHash": "0xe3649d1d6a51572d2f6a0f82683d54eb3717dae3373f9a0c2a3648c392e66433" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index dcb729480be..18ef4316389 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -267,7 +267,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { { _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - proposalHash_ = _hashProposal(_targets, _values, _calldatas, _description); + proposalHash_ = bytes32(0); // TODO: Implement hashProposalWithModule ProposalData storage proposal = _proposals[proposalHash_]; if (proposal.proposer != address(0)) { @@ -327,7 +327,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (uint256 governorProposalId_) { // Verify that the provided data matches the proposalHash - bytes32 _proposalHash = _hashProposal(_targets, _values, _calldatas, _description); + bytes32 _proposalHash = bytes32(0); // TODO: Implement hashProposalWithModule ProposalData storage proposal = _proposals[_proposalHash]; @@ -454,17 +454,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); } - function _hashProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. + /// @param _module The address of the voting module to use for this proposal. + /// @param _proposalData The proposal data to pass to the voting module. + /// @param _descriptionHash The hash of the proposal description. + /// @return The hash of the proposal. + function _hashProposalWithModule( + address _module, + bytes memory _proposalData, + bytes32 _descriptionHash ) internal - pure - returns (bytes32 proposalHash_) + view + returns (bytes32) { - return keccak256(abi.encode(_targets, _values, _calldatas, _description)); + return keccak256(abi.encode(address(this), _module, _proposalData, _descriptionHash)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 1c6745438ad..3b6c8f36973 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -30,17 +30,16 @@ contract ProposalValidatorForTest is ProposalValidator { ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) { } - function hashProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + function hashProposalWithModule( + address _module, + bytes memory _proposalData, + bytes32 _descriptionHash ) public - pure + view returns (bytes32) { - return _hashProposal(_targets, _values, _calldatas, _description); + return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } } @@ -254,7 +253,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); + bytes32 expectedProposalHash = bytes32(0); // TODO: Implement hashProposalWithModule // Expect event to be emitted vm.expectEmit(address(validator)); @@ -678,6 +677,42 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { } } +/// @title ProposalValidator_HashProposalWithModule_Test +/// @notice Tests for the hashProposalWithModule function +contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { + function test_hashProposalWithModule_succeeds() public { + address testModule = makeAddr("testModule"); + bytes memory testProposalData = abi.encode("test", "proposal", "data"); + bytes32 testDescriptionHash = keccak256("test description"); + + bytes32 hash = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + assertTrue(hash != bytes32(0)); + } + + function test_hashProposalWithModule_consistentHash_succeeds() public { + address testModule = makeAddr("testModule"); + bytes memory testProposalData = abi.encode("test data"); + bytes32 testDescriptionHash = keccak256("description"); + + bytes32 hash1 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + bytes32 hash2 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + + assertEq(hash1, hash2); + } + + function test_hashProposalWithModule_differentInputs_succeeds() public { + address module1 = makeAddr("module1"); + address module2 = makeAddr("module2"); + bytes memory data = abi.encode("data"); + bytes32 descHash = keccak256("desc"); + + bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); + bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); + + assertTrue(hash1 != hash2); + } +} + /// @title ProposalValidator_Initialize_Test /// @notice Tests for the initialize function contract ProposalValidator_Initialize_Test is ProposalValidator_Init { From f48a1a859a3eb8bc5e26fc1d43f636fad6853a01 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:54:22 -0300 Subject: [PATCH 09/73] chore: remove submit proposal (#409) * chore: remove submitProposal function * test: remove usage of submitProposal function --- .../governance/IProposalValidator.sol | 20 --- .../snapshots/abi/ProposalValidator.json | 99 ------------ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 128 ++------------- .../test/governance/ProposalValidator.t.sol | 151 +----------------- 5 files changed, 24 insertions(+), 378 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7ab9b4194a5..123aa8421fd 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -40,17 +40,6 @@ interface IProposalValidator is ISemver { CouncilBudget } - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, - string description, - ProposalType proposalType, - uint8 proposalVotingModule - ); - event ProposalApproved( bytes32 indexed proposalHash, address indexed approver @@ -78,15 +67,6 @@ interface IProposalValidator is ISemver { event Initialized(uint8 version); - function submitProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description, - ProposalType _proposalType, - bytes32 _attestationUid - ) external returns (bytes32 proposalHash_); - function approveProposal(bytes32 _proposalHash) external; function moveToVote( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index acb1665cd84..aa92ed7bf73 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -357,50 +357,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_values", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "_calldatas", - "type": "bytes[]" - }, - { - "internalType": "string", - "name": "_description", - "type": "string" - }, - { - "internalType": "enum ProposalValidator.ProposalType", - "name": "_proposalType", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "_attestationUid", - "type": "bytes32" - } - ], - "name": "submitProposal", - "outputs": [ - { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -552,61 +508,6 @@ "name": "ProposalMovedToVote", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "proposer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "indexed": false, - "internalType": "bytes[]", - "name": "calldatas", - "type": "bytes[]" - }, - { - "indexed": false, - "internalType": "string", - "name": "description", - "type": "string" - }, - { - "indexed": false, - "internalType": "enum ProposalValidator.ProposalType", - "name": "proposalType", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "proposalVotingModule", - "type": "uint8" - } - ], - "name": "ProposalSubmitted", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index e769c025436..9befd5699f1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x553e9a5abda992f985f23d02f07c169be7ee39063d6dbd00742b4298089d3602", - "sourceCodeHash": "0xe3649d1d6a51572d2f6a0f82683d54eb3717dae3373f9a0c2a3648c392e66433" + "initCodeHash": "0xdcec7b9d2e1d4a7c7849e0b06fe1142fd743fd5927edceb1fc22466bf139d33c", + "sourceCodeHash": "0xf9bed487efd2ee53ab19814bbc32af947a6cb23f891b0a9af9059077155b64dd" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 18ef4316389..f5c28ab509e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -106,26 +106,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { EVENTS //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new proposal is submitted to the validator contract. - /// @param proposalHash The hash of the submitted proposal. - /// @param proposer The address that submitted the proposal. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param calldatas Function data for proposal calls. - /// @param description Description of the proposal. - /// @param proposalType Type of the proposal. - /// @param proposalVotingModule Voting module specific to the proposal type. - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, - string description, - ProposalType proposalType, - uint8 proposalVotingModule - ); - /// @notice Emitted when a delegate approves a proposal. /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. @@ -247,51 +227,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } - /// @notice Submit a proposal for delegate approval - /// @param _targets Target addresses for proposal calls - /// @param _values ETH values for proposal calls - /// @param _calldatas Function data for proposal calls - /// @param _description Description of the proposal - /// @param _proposalType Type of the proposal - /// @return proposalHash_ The hash of the submitted proposal - function submitProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description, - ProposalType _proposalType, - bytes32 _attestationUid - ) - external - returns (bytes32 proposalHash_) - { - _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - - proposalHash_ = bytes32(0); // TODO: Implement hashProposalWithModule - ProposalData storage proposal = _proposals[proposalHash_]; - - if (proposal.proposer != address(0)) { - revert ProposalValidator_ProposalAlreadySubmitted(); - } - - ProposalTypeData memory proposalTypeData = proposalTypesData[_proposalType]; - - proposal.proposer = msg.sender; - proposal.proposalType = _proposalType; - proposal.inVoting = false; - - emit ProposalSubmitted( - proposalHash_, - msg.sender, - _targets, - _values, - _calldatas, - _description, - _proposalType, - proposalTypeData.proposalVotingModule - ); - } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power) /// @param _proposalHash The hash of the proposal to approve function approveProposal(bytes32 _proposalHash) external { @@ -401,57 +336,22 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setProposalTypeData(_proposalType, _proposalTypeData); } - /// @notice Validates a proposal before submission. - /// @dev Checks if the proposal requires approval and validates the attestation. - /// @param _targets Target addresses for proposal calls. - /// @param _values ETH values for proposal calls. - /// @param _calldatas Function data for proposal calls. - /// @param _proposalType Type of the proposal. - /// @param _attestationUid The UID of the attestation proving eligibility. - function _validateProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - ProposalType _proposalType, - bytes32 _attestationUid - ) - private - view - { - if (_requiresAttestation(_proposalType)) { - Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); - if ( - attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID - || !_isValidAttestationData(attestation.data, _proposalType) - ) { - revert ProposalValidator_InvalidAttestation(); - } - } - } - - /// @notice Determines if a proposal type requires approval via attestation. - /// @param _proposalType The type of proposal to check. - /// @return requiresAttestation_ True if the proposal type requires approval, false otherwise. - function _requiresAttestation(ProposalType _proposalType) private pure returns (bool requiresAttestation_) { - return _proposalType == ProposalType.ProtocolOrGovernorUpgrade - || _proposalType == ProposalType.MaintenanceUpgrade || _proposalType == ProposalType.CouncilMemberElections; - } - /// @notice Validates the attestation data for a proposal. - /// @dev Checks that the sender is the approved delegate and that the proposal type is correct. - /// @param _data The attestation data to validate. + /// @dev Checks that the attester is the owner, the schema is correct, + /// the sender is the approved delegate, and that the proposal type is correct. + /// Reverts with ProposalValidator_InvalidAttestation if validation fails. + /// @param _attestationUid The UID of the attestation to validate. /// @param _expectedProposalType The expected proposal type from the attestation. - /// @return isValid_ True if the attestation data is valid, false otherwise. - function _isValidAttestationData( - bytes memory _data, - ProposalType _expectedProposalType - ) - private - view - returns (bool isValid_) - { - (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); - isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); + + if ( + attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) + ) { + revert ProposalValidator_InvalidAttestation(); + } } /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 3b6c8f36973..5d2afb8a99a 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -243,72 +243,6 @@ contract ProposalValidator_Init is CommonTest { } } -/// @title ProposalValidator_SubmitProposal_Test -/// @notice Happy path tests for submitProposal function -contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { - function test_submitProposal_succeeds() public { - (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - bytes32 expectedProposalHash = bytes32(0); // TODO: Implement hashProposalWithModule - - // Expect event to be emitted - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedProposalHash, - topDelegate_A, - _targets, - _values, - _calldatas, - _description, - proposalType, - proposalVotingModule - ); - - // Submit the proposal - vm.prank(topDelegate_A); - bytes32 proposalHash = - validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); - - assertEq(proposalHash, expectedProposalHash); - } -} - -/// @title ProposalValidator_SubmitProposal_TestFail -/// @notice Sad path tests for submitProposal function -contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { - function test_submitProposal_invalidAttestation_reverts() public { - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID - - vm.prank(topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); - } - - function test_submitProposal_wrongAttester_reverts() public { - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - - // Create attestation with wrong delegate - bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); - - vm.prank(topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - } -} - /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { @@ -322,8 +256,8 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_approveProposal_succeeds() public { @@ -362,8 +296,8 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_approveProposal_insufficientVotingPower_reverts() public { @@ -398,8 +332,8 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented _approveProposal(topDelegate_A, proposalHash); _approveProposal(topDelegate_B, proposalHash); @@ -444,8 +378,8 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_moveToVote_insufficientApprovals_reverts() public { @@ -608,75 +542,6 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } } -/// @title ProposalValidator_Integration_Test -/// @notice Integration tests for the full proposal flow -contract ProposalValidator_Integration_Test is ProposalValidator_Init { - function test_proposalFullFlow_succeeds() public { - // Create a proposal - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - - // Expect ProposalSubmitted event - bytes32 expectedProposalHash = keccak256(abi.encode(targets, values, calldatas, description)); - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedProposalHash, - topDelegate_A, - targets, - values, - calldatas, - description, - proposalType, - proposalVotingModule - ); - - vm.prank(topDelegate_A); - bytes32 proposalHash = - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - - // Expect ProposalApproved events for each approval - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_A); - _approveProposal(topDelegate_A, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_B); - _approveProposal(topDelegate_B, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_C); - _approveProposal(topDelegate_C, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_D); - _approveProposal(topDelegate_D, proposalHash); - - // Mock the governor call - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); - - // Expect ProposalMovedToVote event - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(proposalHash, owner); - - // Move to vote phase - vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); - - // It reverts when proposal is already in voting phase - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); - } -} - /// @title ProposalValidator_HashProposalWithModule_Test /// @notice Tests for the hashProposalWithModule function contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { From bebd2e5901572750f9faf3b891240e5131d93705 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:23:08 -0300 Subject: [PATCH 10/73] feat: add submit funding proposal (#411) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * fix: remove duplicated tests * perf: optimiza for loops usage * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr --- .semgrep/rules/sol-rules.yaml | 14 +- .../governance/IOptimismGovernor.sol | 8 + .../governance/IProposalTypesConfigurator.sol | 50 ++ .../governance/IProposalValidator.sol | 40 +- .../snapshots/abi/ProposalValidator.json | 127 +++++ .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 17 +- .../src/governance/ApprovalVotingModule.sol | 522 ++++++++++++++++++ .../src/governance/ProposalValidator.sol | 150 ++++- .../test/governance/ProposalValidator.t.sol | 515 ++++++++++++++++- 10 files changed, 1414 insertions(+), 33 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol create mode 100644 packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index a4f25a2e351..9cf01c69ce1 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -46,6 +46,7 @@ rules: paths: exclude: - packages/contracts-bedrock/test/dispute/WETH98.t.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-safety-natspec-semver-match languages: [generic] @@ -110,6 +111,7 @@ rules: exclude: - packages/contracts-bedrock/test - packages/contracts-bedrock/scripts + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-input-arg-fmt languages: [solidity] @@ -126,6 +128,7 @@ rules: - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol - packages/contracts-bedrock/src/governance/GovernanceToken.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -141,6 +144,7 @@ rules: - packages/contracts-bedrock/scripts/libraries/Solarray.sol - packages/contracts-bedrock/scripts/interfaces/IGnosisSafe.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-doc-comment languages: [solidity] @@ -150,6 +154,7 @@ rules: paths: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -171,6 +176,7 @@ rules: - packages/contracts-bedrock/src/cannon/MIPS2.sol - packages/contracts-bedrock/src/cannon/libraries/MIPSMemory.sol - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-malformed-revert languages: [solidity] @@ -187,6 +193,7 @@ rules: paths: exclude: - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-use-abi-encodecall languages: [solidity] @@ -203,6 +210,7 @@ rules: exclude: - packages/contracts-bedrock/src/L1/OPContractsManager.sol - packages/contracts-bedrock/src/legacy/L1ChugSplashProxy.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-enforce-require-msg languages: [solidity] @@ -214,6 +222,7 @@ rules: paths: exclude: - packages/contracts-bedrock/src/universal/WETH98.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-no-bare-imports languages: [solidity] @@ -223,6 +232,7 @@ rules: paths: exclude: - packages/contracts-bedrock/test + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-error-format languages: [generic] @@ -244,6 +254,7 @@ rules: - packages/contracts-bedrock/src/dispute/lib/Errors.sol - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol @@ -347,6 +358,7 @@ rules: - packages/contracts-bedrock/src/governance/ProposalValidator.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ProposalValidator.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - packages/contracts-bedrock/src/periphery/TransferOnion.sol - packages/contracts-bedrock/src/periphery/faucet/Faucet.sol - packages/contracts-bedrock/src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol @@ -355,4 +367,4 @@ rules: - packages/contracts-bedrock/src/safe/LivenessGuard.sol - packages/contracts-bedrock/src/safe/LivenessModule.sol - packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol - - packages/contracts-bedrock/src/universal/ReinitializableBase.sol + - packages/contracts-bedrock/src/universal/ReinitializableBase.sol \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index fd9e773b466..39dd12b664a 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {VotingModule} from "src/governance/VotingModule.sol"; + interface IOptimismGovernor { function propose( address[] memory targets, @@ -17,4 +18,11 @@ interface IOptimismGovernor { string memory description, uint8 proposalType ) external returns (uint256 proposalId); + + function timelock() external view returns (address); + + /// @notice Returns the snapshot block number for a proposal, 0 if proposal doesn't exist + /// @param proposalId The ID of the proposal + /// @return The snapshot block number, or 0 if proposal doesn't exist + function proposalSnapshot(uint256 proposalId) external view returns (uint256); } \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol new file mode 100644 index 00000000000..17e92cd5d5b --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IProposalTypesConfigurator { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error InvalidQuorum(); + error InvalidApprovalThreshold(); + error NotManagerOrTimelock(); + error AlreadyInit(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event ProposalTypeSet( + uint8 indexed proposalTypeId, uint16 quorum, uint16 approvalThreshold, string name, string description + ); + + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + struct ProposalType { + uint16 quorum; + uint16 approvalThreshold; + string name; + string description; + address module; + } + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function initialize(address _governor, ProposalType[] calldata _proposalTypes) external; + + function proposalTypes(uint8 proposalTypeId) external view returns (ProposalType memory); + + function setProposalType( + uint8 proposalTypeId, + uint16 quorum, + uint16 approvalThreshold, + string memory name, + string memory description, + address module + ) external; +} diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 123aa8421fd..4bae770c23c 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import {IGovernanceToken} from './IGovernanceToken.sol'; import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. @@ -18,6 +19,9 @@ interface IProposalValidator is ISemver { error ProposalValidator_VotingCycleAlreadySet(); error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); + error ProposalValidator_InvalidFundingProposalType(); + error ProposalValidator_ExceedsDistributionThreshold(); + error ProposalValidator_InvalidOptionsLength(); struct ProposalData { address proposer; @@ -56,7 +60,16 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + event ProposalTypeDataSet( + ProposalType proposalType, + uint256 requiredApprovals, + uint8 proposalVotingModule + ); + + event ProposalVotingModuleData( + bytes32 indexed proposalHash, + bytes encodedVotingModuleData + ); event VotingCycleDataSet( uint256 cycleNumber, @@ -64,6 +77,13 @@ interface IProposalValidator is ISemver { uint256 duration, uint256 votingCycleDistributionLimit ); + + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + string description, + ProposalType proposalType + ); event Initialized(uint8 version); @@ -79,7 +99,7 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; - + function setProposalTypeData( ProposalType _proposalType, ProposalTypeData memory _proposalTypeData @@ -91,9 +111,19 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit ) external; - + + function submitFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) external returns (bytes32 proposalHash_); + function initialize( address _owner, + IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, @@ -123,9 +153,11 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); - + function votingCycles(uint256) external view returns ( uint256 startingBlock, uint256 duration, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index aa92ed7bf73..4f96acab934 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -124,6 +124,11 @@ "name": "_owner", "type": "address" }, + { + "internalType": "contract IProposalTypesConfigurator", + "name": "_proposalTypesConfigurator", + "type": "address" + }, { "internalType": "uint256", "name": "_minimumVotingPower", @@ -242,6 +247,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proposalTypesConfigurator", + "outputs": [ + { + "internalType": "contract IProposalTypesConfigurator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -357,6 +375,50 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, + { + "internalType": "address[]", + "name": "_optionsRecipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_optionsAmounts", + "type": "uint256[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "submitFundingProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -508,6 +570,37 @@ "name": "ProposalMovedToVote", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + } + ], + "name": "ProposalSubmitted", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -533,6 +626,25 @@ "name": "ProposalTypeDataSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "encodedVotingModuleData", + "type": "bytes" + } + ], + "name": "ProposalVotingModuleData", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -564,6 +676,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_ExceedsDistributionThreshold", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", @@ -579,6 +696,16 @@ "name": "ProposalValidator_InvalidAttestation", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidFundingProposalType", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidOptionsLength", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 9befd5699f1..f2b2eb807c7 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xdcec7b9d2e1d4a7c7849e0b06fe1142fd743fd5927edceb1fc22466bf139d33c", - "sourceCodeHash": "0xf9bed487efd2ee53ab19814bbc32af947a6cb23f891b0a9af9059077155b64dd" + "initCodeHash": "0xa3010da5a7dd34d0256de17d400b9b39ded7339acbd33a2e609a2ef2b6140be5", + "sourceCodeHash": "0xbb7d2b4bb9b9789f27b585751da0092ac117615fab7019086c9af3ab0d43311f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 8b940980418..8e422ff0af2 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,39 +34,46 @@ "slot": "52", "type": "uint256[49]" }, + { + "bytes": "20", + "label": "proposalTypesConfigurator", + "offset": 0, + "slot": "101", + "type": "contract IProposalTypesConfigurator" + }, { "bytes": "32", "label": "minimumVotingPower", "offset": 0, - "slot": "101", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "102", + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "103", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "104", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "105", + "slot": "106", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol new file mode 100644 index 00000000000..7f189d3f6ec --- /dev/null +++ b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { EnumerableSetUpgradeable } from + "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { VotingModule } from "./VotingModule.sol"; + +enum VoteType { + Against, + For, + Abstain +} + +enum PassingCriteria { + Threshold, + TopChoices +} + +struct ExecuteParams { + address targets; + uint256 values; + bytes calldatas; +} + +struct ProposalSettings { + uint8 maxApprovals; + uint8 criteria; + address budgetToken; + uint128 criteriaValue; + uint128 budgetAmount; +} + +struct ProposalOption { + uint256 budgetTokensSpent; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; +} + +struct Proposal { + address governor; + uint256 initBalance; + uint128[] optionVotes; + ProposalOption[] options; + ProposalSettings settings; +} + +contract ApprovalVotingModule is VotingModule { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error WrongProposalId(); + error MaxChoicesExceeded(); + error MaxApprovalsExceeded(); + error BudgetExceeded(); + error OptionsNotStrictlyAscending(); + + /*////////////////////////////////////////////////////////////// + LIBRARIES + //////////////////////////////////////////////////////////////*/ + + using SafeCastLib for uint256; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => Proposal) public proposals; + mapping(uint256 => mapping(address => EnumerableSetUpgradeable.UintSet)) private accountVotesSet; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) VotingModule(_governor) { } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * Save settings and options for a new proposal. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. + */ + function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external override { + _onlyGovernor(); + if (proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), proposalData, descriptionHash)))) { + revert WrongProposalId(); + } + + if (proposals[proposalId].governor != address(0)) { + revert ExistingProposal(); + } + + (ProposalOption[] memory proposalOptions, ProposalSettings memory proposalSettings) = + abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + uint256 optionsLength = proposalOptions.length; + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert InvalidParams(); + } + if (proposalSettings.criteria == uint8(PassingCriteria.TopChoices)) { + if (proposalSettings.criteriaValue > optionsLength) { + revert MaxChoicesExceeded(); + } + } + + unchecked { + // Ensure proposal params of each option have the same length between themselves + ProposalOption memory option; + for (uint256 i; i < optionsLength; ++i) { + option = proposalOptions[i]; + if (option.targets.length != option.values.length || option.targets.length != option.calldatas.length) { + revert InvalidParams(); + } + + proposals[proposalId].options.push(option); + } + } + + proposals[proposalId].governor = msg.sender; + proposals[proposalId].settings = proposalSettings; + proposals[proposalId].optionVotes = new uint128[](optionsLength); + } + + /** + * Count approvals voted by `account`. If voting for, options need to be set in ascending order. Votes can only be + * cast once. + * + * @param proposalId The id of the proposal. + * @param account The account to count votes for. + * @param support The type of vote to count. + * @param weight The total vote weight of the `account`. + * @param params The ids of the options to vote for sorted in ascending order, encoded as `uint256[]`. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) + external + virtual + override + { + _onlyGovernor(); + Proposal memory proposal = proposals[proposalId]; + + if (support == uint8(VoteType.For)) { + if (weight != 0) { + uint256[] memory options = _decodeVoteParams(params); + uint256 totalOptions = options.length; + if (totalOptions == 0) revert InvalidParams(); + + _recordVote( + proposalId, account, weight.toUint128(), options, totalOptions, proposal.settings.maxApprovals + ); + } + } + } + + /** + * Format executeParams for a governor, given `proposalId` and `proposalData`. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. + * @return targets The targets of the proposal. + * @return values The values of the proposal. + * @return calldatas The calldatas of the proposal. + */ + function _formatExecuteParams( + uint256 proposalId, + bytes memory proposalData + ) + public + override + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) + { + _onlyGovernor(); + (ProposalOption[] memory options, ProposalSettings memory settings) = + abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + { + IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); + + // If budgetToken is not ETH + if (settings.budgetToken != address(0)) { + // Save initBalance to be used as comparison in `_afterExecute` + proposals[proposalId].initBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); + } + } + + (uint128[] memory sortedOptionVotes, ProposalOption[] memory sortedOptions) = + _sortOptions(proposals[proposalId].optionVotes, options); + + (uint256 executeParamsLength, uint256 succeededOptionsLength) = + _countOptions(sortedOptions, sortedOptionVotes, settings); + + ExecuteParams[] memory executeParams = new ExecuteParams[](executeParamsLength); + executeParamsLength = 0; + uint256 n; + uint256 totalValue; + ProposalOption memory option; + + { + bool budgetExceeded = false; + + // Flatten `options` by filling `executeParams` until budgetAmount is exceeded + for (uint256 i; i < succeededOptionsLength;) { + option = sortedOptions[i]; + + for (n = 0; n < option.targets.length;) { + // If `budgetToken` is ETH and value is not zero, add transaction value to `totalValue` + if (settings.budgetToken == address(0) && option.values[n] != 0) { + if (totalValue + option.values[n] > settings.budgetAmount) { + budgetExceeded = true; + break; // break inner loop + } + totalValue += option.values[n]; + } + + unchecked { + executeParams[executeParamsLength + n] = + ExecuteParams(option.targets[n], option.values[n], option.calldatas[n]); + + ++n; + } + } + + // If `budgetAmount` for ETH is exceeded, skip option. + if (budgetExceeded) break; + + // Check if budgetAmount is exceeded for non-ETH tokens + if (settings.budgetToken != address(0) && settings.budgetAmount != 0) { + if (option.budgetTokensSpent != 0) { + if (totalValue + option.budgetTokensSpent > settings.budgetAmount) break; // break outer loop + // for non-ETH tokens + totalValue += option.budgetTokensSpent; + } + } + + unchecked { + executeParamsLength += n; + + ++i; + } + } + } + + unchecked { + // Increase by one to account for additional `_afterExecute` call + uint256 effectiveParamsLength = executeParamsLength + 1; + + // Init params lengths + targets = new address[](effectiveParamsLength); + values = new uint256[](effectiveParamsLength); + calldatas = new bytes[](effectiveParamsLength); + } + + // Set n `targets`, `values` and `calldatas` + for (uint256 i; i < executeParamsLength;) { + targets[i] = executeParams[i].targets; + values[i] = executeParams[i].values; + calldatas[i] = executeParams[i].calldatas; + + unchecked { + ++i; + } + } + + // Set `_afterExecute` as last call + targets[executeParamsLength] = address(this); + values[executeParamsLength] = 0; + calldatas[executeParamsLength] = + abi.encodeWithSelector(this._afterExecute.selector, proposalId, proposalData, totalValue); + } + + /** + * Hook called by a governor after execute, for `proposalId` with `proposalData`. + * Revert if the transaction has resulted in more tokens being spent than `budgetAmount`. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. + * @param budgetTokensSpent The total amount of tokens that can be spent. + */ + function _afterExecute(uint256 proposalId, bytes memory proposalData, uint256 budgetTokensSpent) public view { + (, ProposalSettings memory settings) = abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + if (settings.budgetToken != address(0) && settings.budgetAmount > 0) { + IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); + + uint256 initBalance = proposals[proposalId].initBalance; + uint256 finalBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); + + // If `finalBalance` is higher than `initBalance`, ignore the budget check + if (finalBalance < initBalance) { + /// @dev Cannot underflow as `finalBalance` is less than `initBalance` + unchecked { + if (initBalance - finalBalance > budgetTokensSpent) { + revert BudgetExceeded(); + } + } + } + } + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * Return the ids of the options voted by `account` on `proposalId`. + */ + function getAccountVotes(uint256 proposalId, address account) external view returns (uint256[] memory) { + return accountVotesSet[proposalId][account].values(); + } + + /** + * Return the total number of votes cast by `account` on `proposalId`. + */ + function getAccountTotalVotes(uint256 proposalId, address account) external view returns (uint256) { + return accountVotesSet[proposalId][account].length(); + } + + /** + * @dev Return true if at least one option satisfies the passing criteria. + * Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. + * + * @param proposalId The id of the proposal. + */ + function _voteSucceeded(uint256 proposalId) external view override returns (bool) { + Proposal memory proposal = proposals[proposalId]; + + ProposalOption[] memory options = proposal.options; + uint256 n = options.length; + unchecked { + if (proposal.settings.criteria == uint8(PassingCriteria.Threshold)) { + for (uint256 i; i < n; ++i) { + if (proposal.optionVotes[i] >= proposal.settings.criteriaValue) return true; + } + } else if (proposal.settings.criteria == uint8(PassingCriteria.TopChoices)) { + for (uint256 i; i < n; ++i) { + if (proposal.optionVotes[i] != 0) return true; + } + } + } + + return false; + } + + /** + * Defines the encoding for the expected `proposalData` in `propose`. + * Encoding: `(ProposalOption[], ProposalSettings)` + * + * @dev Can be used by clients to interact with modules programmatically without prior knowledge + * on expected types. + */ + function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { + return + "((uint256 budgetTokensSpent,address[] targets,uint256[] values,bytes[] calldatas,string description)[] proposalOptions,(uint8 maxApprovals,uint8 criteria,address budgetToken,uint128 criteriaValue,uint128 budgetAmount) proposalSettings)"; + } + + /** + * Defines the encoding for the expected `params` in `_countVote`. + * Encoding: `uint256[]` + * + * @dev Can be used by clients to interact with modules programmatically without prior knowledge + * on expected types. + */ + function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { + return "uint256[] optionIds"; + } + + /** + * @dev See {IGovernor-COUNTING_MODE}. + * + * - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. + * - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. + * - `params=approvalVote`: params needs to be formatted as `VOTE_PARAMS_ENCODING`. + */ + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo&quorum=against,for,abstain¶ms=approvalVote"; + } + + /** + * Module version. + */ + function version() public pure returns (uint256) { + return 1; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _recordVote( + uint256 proposalId, + address account, + uint128 weight, + uint256[] memory options, + uint256 totalOptions, + uint256 maxApprovals + ) + internal + { + uint256 option; + uint256 prevOption; + for (uint256 i; i < totalOptions;) { + option = options[i]; + + accountVotesSet[proposalId][account].add(option); + + // Revert if `option` is not strictly ascending + if (i != 0) { + if (option <= prevOption) revert OptionsNotStrictlyAscending(); + } + + prevOption = option; + + /// @dev Revert if `option` is out of bounds + proposals[proposalId].optionVotes[option] += weight; + + unchecked { + ++i; + } + } + + if (accountVotesSet[proposalId][account].length() > maxApprovals) { + revert MaxApprovalsExceeded(); + } + } + + // Sort `options` by `optionVotes` in descending order + function _sortOptions( + uint128[] memory optionVotes, + ProposalOption[] memory options + ) + internal + pure + returns (uint128[] memory, ProposalOption[] memory) + { + unchecked { + uint128 highestValue; + ProposalOption memory highestOption; + uint256 index; + + for (uint256 i; i < optionVotes.length - 1; ++i) { + highestValue = optionVotes[i]; + + for (uint256 j = i + 1; j < optionVotes.length; ++j) { + if (optionVotes[j] > highestValue) { + highestValue = optionVotes[j]; + index = j; + } + } + + if (index != 0) { + optionVotes[index] = optionVotes[i]; + optionVotes[i] = highestValue; + + highestOption = options[index]; + options[index] = options[i]; + options[i] = highestOption; + + index = 0; + } + } + + return (optionVotes, options); + } + } + + // Derive `executeParamsLength` and `succeededOptionsLength` based on passing criteria + function _countOptions( + ProposalOption[] memory options, + uint128[] memory optionVotes, + ProposalSettings memory settings + ) + internal + pure + returns (uint256 executeParamsLength, uint256 succeededOptionsLength) + { + uint256 n = options.length; + unchecked { + uint256 i; + if (settings.criteria == uint8(PassingCriteria.Threshold)) { + // if criteria is `Threshold`, loop through options until `optionVotes` is less than threshold + for (i; i < n; ++i) { + if (optionVotes[i] >= settings.criteriaValue) { + executeParamsLength += options[i].targets.length; + } else { + break; + } + } + } else if (settings.criteria == uint8(PassingCriteria.TopChoices)) { + // if criteria is `TopChoices`, loop through options until the top choices are filled + for (i; i < settings.criteriaValue; ++i) { + if (optionVotes[i] > 0) { + executeParamsLength += options[i].targets.length; + } else { + break; + } + } + } + succeededOptionsLength = i; + } + } + + // Virtual method used to decode _countVote params. + function _decodeVoteParams(bytes memory params) internal virtual returns (uint256[] memory options) { + options = abi.decode(params, (uint256[])); + } +} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f5c28ab509e..a74046a468e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -11,9 +11,14 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +// Modules +import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; + /// @custom:proxied true /// @title ProposalValidator /// @notice The ProposalValidator contract is responsible for validating proposals and moving @@ -47,6 +52,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the length of the proposal types and proposal types data arrays do not match. error ProposalValidator_ProposalTypesDataLengthMismatch(); + /// @notice Thrown when the proposal type is not valid for funding proposals. + error ProposalValidator_InvalidFundingProposalType(); + + /// @notice Thrown when the requested amount exceeds the distribution threshold. + error ProposalValidator_ExceedsDistributionThreshold(); + + /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). + error ProposalValidator_InvalidOptionsLength(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -68,7 +82,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing explicit data for each proposal type. /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. - /// @param proposalVotingModule The voting module each proposal type must use. + /// @param proposalVotingModule The proposal type ID used to get the voting module from the configurator. struct ProposalTypeData { uint256 requiredApprovals; uint8 proposalVotingModule; @@ -106,6 +120,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { EVENTS //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a new proposal is submitted. + /// @param proposalHash The hash of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + ); + /// @notice Emitted when a delegate approves a proposal. /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. @@ -136,9 +159,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal voting module. + /// @param proposalVotingModule The proposal type ID. event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + /// @notice Emitted with ProposalSubmitted event. + /// @param proposalHash The hash of the submitted proposal. + /// @param encodedVotingModuleData The encoded voting module data. + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } bytes32 public immutable ATTESTATION_SCHEMA_UID; @@ -149,6 +177,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The token used to determine voting power. IGovernanceToken public immutable VOTING_TOKEN; + /// @notice The proposal types configurator contract. + IProposalTypesConfigurator public proposalTypesConfigurator; + /// @notice The minimum voting power required for a delegate to approve proposals. uint256 public minimumVotingPower; @@ -162,7 +193,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { mapping(ProposalType => ProposalTypeData) public proposalTypesData; /// @notice Mapping of proposal hash to their corresponding proposal data. - mapping(bytes32 => ProposalData) private _proposals; + mapping(bytes32 => ProposalData) internal _proposals; /// @notice Semantic version. /// @custom:semver 1.0.0-beta.1 @@ -189,6 +220,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. + /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _cycleNumber The number of the current voting cycle. /// @param _startBlock The block number of the starting block of the voting cycle. @@ -199,6 +231,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, + IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, @@ -215,6 +248,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } + proposalTypesConfigurator = _proposalTypesConfigurator; _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); @@ -227,6 +261,112 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and + /// voting. + /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), + /// then convert to the absolute vote count by calculating: (percentage / 100) * total_votable_supply. + /// Direct contract callers must provide the absolute number of votes required for passage. + /// @param _criteriaValue The absolute number of votes required for the proposal to pass. This represents the + /// threshold that must be met or exceeded for any option to be considered successful. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _optionsRecipients An address for each option to transfer funds to in case the option passes the voting. + /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. + /// @param _description Description of the proposal. + /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @return proposalHash_ The hash of the submitted proposal. + function submitFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + // Only funding proposal types can use this function + if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { + revert ProposalValidator_InvalidFundingProposalType(); + } + + // Validate input arrays have matching lengths + uint256 optionsLength = _optionsDescriptions.length; + if (optionsLength != _optionsRecipients.length || optionsLength != _optionsAmounts.length) { + revert ProposalValidator_ProposalTypesDataLengthMismatch(); + } + + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + + ProposalOption[] memory options = new ProposalOption[](optionsLength); + uint256 totalBudget = 0; + + // Check amounts, build options, and calculate total budget in single loop + for (uint256 i = 0; i < optionsLength; i++) { + if (_optionsAmounts[i] > distributionThreshold) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (_optionsRecipients[i], _optionsAmounts[i])); + + options[i] = ProposalOption({ + budgetTokensSpent: _optionsAmounts[i], + targets: targets, + values: values, + calldatas: calldatas, + description: _optionsDescriptions[i] + }); + + totalBudget += _optionsAmounts[i]; + } + + // Configure approval voting settings + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: _criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals with same hash + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = _proposalType; + proposal.inVoting = false; + + emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + } + /// @notice Approve a proposal (only callable by delegates with sufficient voting power) /// @param _proposalHash The hash of the proposal to approve function approveProposal(bytes32 _proposalHash) external { @@ -282,7 +422,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalVotingModule); + GOVERNOR.propose(_targets, _values, _calldatas, _description, uint8(proposal.proposalType)); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -368,7 +508,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { view returns (bytes32) { - return keccak256(abi.encode(address(this), _module, _proposalData, _descriptionHash)); + return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 5d2afb8a99a..2731ecb6442 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -5,9 +5,11 @@ pragma solidity 0.8.15; import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; @@ -16,7 +18,11 @@ import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +// Modules +import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; + // Testing utilities +import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest @@ -41,11 +47,22 @@ contract ProposalValidatorForTest is ProposalValidator { { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } + + function getProposalData(bytes32 _proposalHash) + public + view + returns (address proposer_, ProposalType proposalType_, bool inVoting_, uint256 approvalCount_) + { + ProposalData storage proposal = _proposals[_proposalHash]; + return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); + } } /// @title ProposalValidator_Init /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { + using stdStorage for StdStorage; + uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; @@ -61,22 +78,20 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_B; address topDelegate_C; address topDelegate_D; + address approvalVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; IOptimismGovernor public governor; + IProposalTypesConfigurator public proposalTypesConfigurator; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; event ProposalSubmitted( bytes32 indexed proposalHash, address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, string description, - ProposalValidator.ProposalType proposalType, - uint8 proposalVotingModule + ProposalValidator.ProposalType proposalType ); event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); @@ -110,6 +125,50 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } + /// @notice Helper function to set proposal type data using StdStorage. + function _setProposalTypeData( + ProposalValidator.ProposalType _proposalType, + ProposalValidator.ProposalTypeData memory _data + ) + internal + { + // Set requiredApprovals (depth 0) + stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(0) + .checked_write(_data.requiredApprovals); + + // Set proposalVotingModule (depth 1) + stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(1) + .checked_write(_data.proposalVotingModule); + } + + /// @notice Helper function to set GovernanceFund proposal type data. + function _setGovernanceFundProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.GovernanceFund, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 3 + }) + ); + } + + /// @notice Helper function to set CouncilBudget proposal type data. + function _setCouncilBudgetProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.CouncilBudget, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 4 + }) + ); + } + + /// @notice Helper function to set both funding proposal types. + function _setFundingProposalTypes() internal { + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); + } + function _getProposalTypesAndData() internal pure @@ -129,24 +188,94 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 1 }); proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 2 }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 3 }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 4 }); return (proposalTypes, proposalTypesData); } + function _constructVotingModuleData( + string[] memory descriptions, + address[] memory recipients, + uint256[] memory amounts, + uint128 criteriaValue + ) + internal + pure + returns (bytes memory) + { + // Construct ProposalOption array + ProposalOption[] memory options = new ProposalOption[](descriptions.length); + + for (uint256 i = 0; i < descriptions.length; i++) { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i])); + + options[i] = ProposalOption({ + budgetTokensSpent: amounts[i], + targets: targets, + values: values, + calldatas: calldatas, + description: descriptions[i] + }); + } + + // Calculate total budget + uint256 totalBudget = 0; + for (uint256 i = 0; i < amounts.length; i++) { + totalBudget += amounts[i]; + } + + // Construct ProposalSettings + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(descriptions.length), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + return abi.encode(options, settings); + } + + /// @notice Helper function to setup proposal types configurator mocks + function _setupProposalTypesConfiguratorMocks() internal { + // Mock calls for different proposal type IDs + for (uint8 i = 0; i < 5; i++) { + address moduleAddress = (i == 3 || i == 4) ? approvalVotingModule : address(0); + + vm.mockCall( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (i)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 100, + approvalThreshold: 100, + name: "Test Proposal Type", + description: "Test Description", + module: moduleAddress + }) + ) + ); + } + } + /// @notice Initializes the validator function _initializeValidator() internal virtual { ( @@ -154,8 +283,13 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalTypeData[] memory proposalTypesData ) = _getProposalTypesAndData(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); + + // Setup mocks + _setupProposalTypesConfiguratorMocks(); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -165,6 +299,7 @@ contract ProposalValidator_Init is CommonTest { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, @@ -184,6 +319,7 @@ contract ProposalValidator_Init is CommonTest { owner = governanceToken.owner(); rando = makeAddr("rando"); governor = IOptimismGovernor(makeAddr("governor")); + approvalVotingModule = makeAddr("approvalVotingModule"); vm.prank(owner); ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( @@ -507,7 +643,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setProposalTypeData_succeeds( uint8 proposalTypeValue, uint256 newRequiredApprovals, - uint8 newConfigurator + uint8 newProposalTypeId ) public { @@ -517,19 +653,19 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalVotingModule: newConfigurator + proposalVotingModule: newProposalTypeId }); // Expect the ProposalTypeDataSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newConfigurator); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); vm.prank(owner); validator.setProposalTypeData(proposalType, newData); (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newConfigurator); + assertEq(proposalVotingModule, newProposalTypeId); } function test_setProposalTypeData_notOwner_reverts() public { @@ -578,11 +714,356 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init } } +/// @title ProposalValidator_SubmitFundingProposal_Test +/// @notice Happy path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + string description; + + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + + function setUp() public override { + super.setUp(); + + _setFundingProposalTypes(); + + criteriaValue = 1000 ether; + } + + function testFuzz_submitFundingProposal_succeeds( + uint8 proposalTypeValue, + uint8 optionCount, + uint256 amount, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Bound option count between 1 and 50 for reasonable test execution + optionCount = uint8(bound(optionCount, 1, 50)); + + // Bound amount from 0 to DISTRIBUTION_THRESHOLD (inclusive) + amount = bound(amount, 0, DISTRIBUTION_THRESHOLD); + + // Create arrays based on option count + string[] memory descriptions = new string[](optionCount); + address[] memory recipients = new address[](optionCount); + uint256[] memory amounts = new uint256[](optionCount); + + for (uint256 i = 0; i < optionCount; i++) { + descriptions[i] = string(abi.encodePacked("Option ", vm.toString(i))); + recipients[i] = makeAddr(string(abi.encodePacked("recipient", vm.toString(i)))); + amounts[i] = amount; // Use the same bounded amount for all options + } + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructVotingModuleData(descriptions, recipients, amounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Expect ProposalSubmitted event + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, description, proposalType); + + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(inVoting, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitFundingProposal_TestFail +/// @notice Sad path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + string description; + + function setUp() public override { + super.setUp(); + + // Set GovernanceFund to use the approval voting module + _setGovernanceFundProposalType(); + + criteriaValue = 50; + optionsDescriptions = new string[](2); + optionsDescriptions[0] = "Option A"; + optionsDescriptions[1] = "Option B"; + + optionsRecipients = new address[](2); + optionsRecipients[0] = makeAddr("recipient1"); + optionsRecipients[1] = makeAddr("recipient2"); + + optionsAmounts = new uint256[](2); + optionsAmounts[0] = 1000 ether; + optionsAmounts[1] = 500 ether; + + description = "Test funding proposal"; + } + + function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Bound to proposal types that are NOT funding proposals (0, 1, 2) + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, description, proposalType + ); + } + + function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - recipients and amounts match, descriptions are different + string[] memory mismatchedDescriptions = new string[](mismatchedLength); + address[] memory matchingRecipients = new address[](matchingLength); + uint256[] memory matchingAmounts = new uint256[](matchingLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + mismatchedDescriptions, + matchingRecipients, + matchingAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_mismatchedRecipientsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - descriptions and amounts match, recipients are different + string[] memory matchingDescriptions = new string[](matchingLength); + address[] memory mismatchedRecipients = new address[](mismatchedLength); + uint256[] memory matchingAmounts = new uint256[](matchingLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + matchingDescriptions, + mismatchedRecipients, + matchingAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_mismatchedAmountsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - descriptions and recipients match, amounts are different + string[] memory matchingDescriptions = new string[](matchingLength); + address[] memory matchingRecipients = new address[](matchingLength); + uint256[] memory mismatchedAmounts = new uint256[](mismatchedLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + matchingDescriptions, + matchingRecipients, + mismatchedAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts(uint256 excessAmount) public { + // Bound excess amount to be greater than DISTRIBUTION_THRESHOLD + excessAmount = bound(excessAmount, DISTRIBUTION_THRESHOLD + 1, type(uint128).max); + + // Set first option to exceed the threshold + optionsAmounts[0] = excessAmount; + + vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = + _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Submit first proposal + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + + // Attempt to submit identical proposal + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = + _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_zeroOptionsLength_reverts() public { + string[] memory emptyDescriptions = new string[](0); + address[] memory emptyRecipients = new address[](0); + uint256[] memory emptyAmounts = new uint256[](0); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + emptyDescriptions, + emptyRecipients, + emptyAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_exceedsMaxOptionsLength_reverts() public { + // Create arrays with 256 options (exceeds uint8 max of 255) + uint256 tooManyOptions = 256; + string[] memory tooManyDescriptions = new string[](tooManyOptions); + address[] memory tooManyRecipients = new address[](tooManyOptions); + uint256[] memory tooManyAmounts = new uint256[](tooManyOptions); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + tooManyDescriptions, + tooManyRecipients, + tooManyAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } +} + /// @title ProposalValidator_Initialize_Test /// @notice Tests for the initialize function contract ProposalValidator_Initialize_Test is ProposalValidator_Init { /// @dev Override to create validator proxy without initialization for testing function _initializeValidator() internal override { + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); + + // Setup mocks + _setupProposalTypesConfiguratorMocks(); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); validator = ProposalValidatorForTest(address(new Proxy(owner))); // Initialize will be tested manually @@ -601,6 +1082,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, @@ -628,7 +1110,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalVotingModule, 0); + assertEq(proposalVotingModule, uint8(i)); } } @@ -646,7 +1128,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 1 }); vm.prank(owner); @@ -657,6 +1139,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, From 315b8e15350146ee0a673e518336f8fc5f9aa601 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:58:44 -0300 Subject: [PATCH 11/73] fix: funding proposal comments (#425) * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr --- .../test/governance/ProposalValidator.t.sol | 239 ++++++++++-------- 1 file changed, 135 insertions(+), 104 deletions(-) diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2731ecb6442..2db50f2c3a6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -71,9 +71,10 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + uint8 public constant FUNDING_PROPOSALS_VOTING_MODULE = 3; address owner; - address rando; + address user; address topDelegate_A; address topDelegate_B; address topDelegate_C; @@ -147,7 +148,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 3 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }) ); } @@ -158,7 +159,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 4 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }) ); } @@ -169,6 +170,22 @@ contract ProposalValidator_Init is CommonTest { _setCouncilBudgetProposalType(); } + /// @notice Helper to create minimal valid arrays for funding proposal error tests + function _createMinimalFundingArrays() + internal + pure + returns (string[] memory descriptions_, address[] memory recipients_, uint256[] memory amounts_) + { + descriptions_ = new string[](1); + descriptions_[0] = "Option A"; + + recipients_ = new address[](1); + recipients_[0] = address(0x1); + + amounts_ = new uint256[](1); + amounts_[0] = 100 ether; + } + function _getProposalTypesAndData() internal pure @@ -196,11 +213,11 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 3 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 4 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }); return (proposalTypes, proposalTypesData); @@ -258,7 +275,7 @@ contract ProposalValidator_Init is CommonTest { function _setupProposalTypesConfiguratorMocks() internal { // Mock calls for different proposal type IDs for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 3 || i == 4) ? approvalVotingModule : address(0); + address moduleAddress = (i == 2 || i == FUNDING_PROPOSALS_VOTING_MODULE) ? approvalVotingModule : address(0); vm.mockCall( address(proposalTypesConfigurator), @@ -317,7 +334,7 @@ contract ProposalValidator_Init is CommonTest { function setUp() public virtual override { super.setUp(); owner = governanceToken.owner(); - rando = makeAddr("rando"); + user = makeAddr("user"); governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); @@ -438,7 +455,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { function test_approveProposal_insufficientVotingPower_reverts() public { vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalHash); + _approveProposal(user, proposalHash); } function test_approveProposal_alreadyApproved_reverts() public { @@ -558,7 +575,7 @@ contract ProposalValidator_Getters_Test is ProposalValidator_Init { bool canSignOff = validator.canSignOff(topDelegate_A); assertTrue(canSignOff); - bool cannotSignOff = validator.canSignOff(rando); + bool cannotSignOff = validator.canSignOff(user); assertFalse(cannotSignOff); } } @@ -578,7 +595,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setMinimumVotingPower_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setMinimumVotingPower(10000 ether); } @@ -609,7 +626,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setVotingCycleData_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setVotingCycleData(2, block.number, 100, 10000 ether); } @@ -635,7 +652,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setDistributionThreshold_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setDistributionThreshold(10000 ether); } @@ -672,7 +689,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } @@ -808,32 +825,13 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init /// @title ProposalValidator_SubmitFundingProposal_TestFail /// @notice Sad path tests for submitFundingProposal function contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - string description; + uint128 public constant FUNDING_CRITERIA_VALUE = 50; + string description = "Test funding proposal"; function setUp() public override { super.setUp(); - - // Set GovernanceFund to use the approval voting module - _setGovernanceFundProposalType(); - - criteriaValue = 50; - optionsDescriptions = new string[](2); - optionsDescriptions[0] = "Option A"; - optionsDescriptions[1] = "Option B"; - - optionsRecipients = new address[](2); - optionsRecipients[0] = makeAddr("recipient1"); - optionsRecipients[1] = makeAddr("recipient2"); - - optionsAmounts = new uint256[](2); - optionsAmounts[0] = 1000 ether; - optionsAmounts[1] = 500 ether; - - description = "Test funding proposal"; + // Set both funding proposal types to use the approval voting module + _setFundingProposalTypes(); } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { @@ -842,16 +840,20 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, "Test proposal", proposalType ); } function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -860,26 +862,31 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - recipients and amounts match, descriptions are different string[] memory mismatchedDescriptions = new string[](mismatchedLength); address[] memory matchingRecipients = new address[](matchingLength); uint256[] memory matchingAmounts = new uint256[](matchingLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, mismatchedDescriptions, matchingRecipients, matchingAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } function testFuzz_submitFundingProposal_mismatchedRecipientsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -888,26 +895,31 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - descriptions and amounts match, recipients are different string[] memory matchingDescriptions = new string[](matchingLength); address[] memory mismatchedRecipients = new address[](mismatchedLength); uint256[] memory matchingAmounts = new uint256[](matchingLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, matchingDescriptions, mismatchedRecipients, matchingAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } function testFuzz_submitFundingProposal_mismatchedAmountsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -916,46 +928,63 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - descriptions and recipients match, amounts are different string[] memory matchingDescriptions = new string[](matchingLength); address[] memory matchingRecipients = new address[](matchingLength); uint256[] memory mismatchedAmounts = new uint256[](mismatchedLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, matchingDescriptions, matchingRecipients, mismatchedAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } - function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts(uint256 excessAmount) public { + function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts( + uint256 excessAmount, + uint8 proposalTypeValue + ) + public + { // Bound excess amount to be greater than DISTRIBUTION_THRESHOLD excessAmount = bound(excessAmount, DISTRIBUTION_THRESHOLD + 1, type(uint128).max); - // Set first option to exceed the threshold - optionsAmounts[0] = excessAmount; + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Create arrays with excessive amount + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + amounts[0] = excessAmount; vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_duplicateProposal_reverts() public { + function testFuzz_submitFundingProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -967,33 +996,30 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); // Submit first proposal - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); // Attempt to submit identical proposal vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_proposalExistsInGovernor_reverts() public { + function testFuzz_submitFundingProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1005,50 +1031,46 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_zeroOptionsLength_reverts() public { + function testFuzz_submitFundingProposal_zeroOptionsLength_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); string[] memory emptyDescriptions = new string[](0); address[] memory emptyRecipients = new address[](0); uint256[] memory emptyAmounts = new uint256[](0); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - emptyDescriptions, - emptyRecipients, - emptyAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, emptyDescriptions, emptyRecipients, emptyAmounts, description, proposalType ); } - function test_submitFundingProposal_exceedsMaxOptionsLength_reverts() public { - // Create arrays with 256 options (exceeds uint8 max of 255) - uint256 tooManyOptions = 256; + function testFuzz_submitFundingProposal_exceedsMaxOptionsLength_reverts( + uint256 tooManyOptions, + uint8 proposalTypeValue + ) + public + { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays with more than 255 options (exceeds allowed uint8 max) + tooManyOptions = uint256(bound(tooManyOptions, 256, 512)); string[] memory tooManyDescriptions = new string[](tooManyOptions); address[] memory tooManyRecipients = new address[](tooManyOptions); uint256[] memory tooManyAmounts = new uint256[](tooManyOptions); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - tooManyDescriptions, - tooManyRecipients, - tooManyAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, tooManyDescriptions, tooManyRecipients, tooManyAmounts, description, proposalType ); } } @@ -1110,7 +1132,16 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalVotingModule, uint8(i)); + + // Both GovernanceFund and CouncilBudget use FUNDING_PROPOSALS_VOTING_MODULE + if ( + proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + ) { + assertEq(proposalVotingModule, FUNDING_PROPOSALS_VOTING_MODULE); + } else { + assertEq(proposalVotingModule, uint8(i)); + } } } From db993638f0d011ac85e7d073262eed24189235ac Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:04:12 -0300 Subject: [PATCH 12/73] feat: add submitCouncilMemberElectionsProposal function (#418) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * feat: add submitCouncilMemberElectionsProposal function * chore: run pre-pr * fix: remove duplicated tests * test(fuzz): use fuzz testing for happy paths * feat: add check for criteria value < optionslength * perf: optimiza for loops usage * test: fuzz invalid attestationUid * test: fuzz invalid proposer * fix: lack of attestation existance validation * test: fuzz exceeded max options test * test: fuzz unapproved attester * test: reduce upper bound for array size * test: remove duplicated test * fix: update criteria value validation logic in ProposalValidator * refactor(test): use helper functions for duplicated logic * refactor: declare approvalVotingModule variable * test: increment the assertions in council memeber election tests * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr * refactor: use variables instead of hardcoded values * chore: rename voting module depending on configurator instead of internal proposal type * chore: improve tests naming --- .../governance/IProposalValidator.sol | 8 + .../snapshots/abi/ProposalValidator.json | 39 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 97 +++++- .../test/governance/ProposalValidator.t.sol | 301 +++++++++++++++++- 5 files changed, 436 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 4bae770c23c..aa787840981 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -22,6 +22,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); + error ProposalValidator_InvalidCriteriaValue(); struct ProposalData { address proposer; @@ -112,6 +113,13 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit ) external; + function submitCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionDescriptions, + string memory _proposalDescription, + bytes32 _attestationUid + ) external returns (bytes32 proposalHash_); + function submitFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 4f96acab934..ec85035250f 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -375,6 +375,40 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionDescriptions", + "type": "string[]" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + } + ], + "name": "submitCouncilMemberElectionsProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -696,6 +730,11 @@ "name": "ProposalValidator_InvalidAttestation", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidCriteriaValue", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidFundingProposalType", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f2b2eb807c7..8ce0da8c95e 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xa3010da5a7dd34d0256de17d400b9b39ded7339acbd33a2e609a2ef2b6140be5", - "sourceCodeHash": "0xbb7d2b4bb9b9789f27b585751da0092ac117615fab7019086c9af3ab0d43311f" + "initCodeHash": "0xe5a13d76f05b05db718ac645ee1aaf8770de307b8f4e367e3def458d1be6ba3f", + "sourceCodeHash": "0x2e2655c25888e6d3502374d7e257888ecc300188efbc18c44b19fc6640b00629" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index a74046a468e..91769149ae3 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -61,6 +61,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). error ProposalValidator_InvalidOptionsLength(); + /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). + error ProposalValidator_InvalidCriteriaValue(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -261,6 +264,93 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a Council Member Elections proposal for approval and voting. + /// @param _criteriaValue Since the passing criteria type is "TopChoices" this number represents the amount + /// of top choices that can pass the voting. + /// @param _optionDescriptions The strings of the different options that can be voted. + /// @param _proposalDescription Description of the proposal. + /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @return proposalHash_ The hash of the submitted proposal. + function submitCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionDescriptions, + string memory _proposalDescription, + bytes32 _attestationUid + ) + external + returns (bytes32 proposalHash_) + { + // Validate EAS attestation - must be called by owner-approved address + _validateAttestation(_attestationUid, ProposalType.CouncilMemberElections); + + // Validate options length bounds + uint256 optionsLength = _optionDescriptions.length; + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + + // Validate criteria value doesn't exceed options length for TopChoices + if (_criteriaValue > optionsLength) { + revert ProposalValidator_InvalidCriteriaValue(); + } + + ProposalOption[] memory options = new ProposalOption[](optionsLength); + + // Build proposal options without any execution calls (elections don't execute operations) + for (uint256 i = 0; i < optionsLength; i++) { + address[] memory targets = new address[](0); + uint256[] memory values = new uint256[](0); + bytes[] memory calldatas = new bytes[](0); + + options[i] = ProposalOption({ + budgetTokensSpent: 0, // No tokens spent for elections + targets: targets, + values: values, + calldatas: calldatas, + description: _optionDescriptions[i] + }); + } + + // Configure approval voting settings with TopChoices criteria + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), // No budget token for elections + criteriaValue: _criteriaValue, + budgetAmount: 0 // No budget amount for elections + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = proposalTypesConfigurator.proposalTypes( + proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule + ).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals with same hash + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = ProposalType.CouncilMemberElections; + + emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + } + /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and /// voting. /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), @@ -361,7 +451,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; - proposal.inVoting = false; emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -484,6 +573,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _expectedProposalType The expected proposal type from the attestation. function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + + // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) + if (attestation.uid == bytes32(0)) { + revert ProposalValidator_InvalidAttestation(); + } + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2db50f2c3a6..12593e24e7b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -11,6 +11,9 @@ import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// Testing utilities +import { stdStorage, StdStorage } from "forge-std/Test.sol"; + // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; import { Proxy } from "src/universal/Proxy.sol"; @@ -48,6 +51,7 @@ contract ProposalValidatorForTest is ProposalValidator { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } + /// @notice Exposes proposal data for testing function getProposalData(bytes32 _proposalHash) public view @@ -56,6 +60,11 @@ contract ProposalValidatorForTest is ProposalValidator { ProposalData storage proposal = _proposals[_proposalHash]; return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); } + + /// @notice Check if a delegate has approved a proposal + function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { + return _proposals[_proposalHash].delegateApprovals[_delegate]; + } } /// @title ProposalValidator_Init @@ -71,7 +80,7 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; - uint8 public constant FUNDING_PROPOSALS_VOTING_MODULE = 3; + uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; address owner; address user; @@ -142,13 +151,24 @@ contract ProposalValidator_Init is CommonTest { .checked_write(_data.proposalVotingModule); } + /// @notice Helper function to set CouncilMemberElections proposal type data. + function _setCouncilMemberElectionsProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.CouncilMemberElections, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: APPROVAL_VOTING_MODULE_ID + }) + ); + } + /// @notice Helper function to set GovernanceFund proposal type data. function _setGovernanceFundProposalType() internal { _setProposalTypeData( ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }) ); } @@ -159,7 +179,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }) ); } @@ -169,7 +189,6 @@ contract ProposalValidator_Init is CommonTest { _setGovernanceFundProposalType(); _setCouncilBudgetProposalType(); } - /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() internal @@ -213,11 +232,11 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); return (proposalTypes, proposalTypesData); @@ -271,11 +290,49 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(options, settings); } + /// @notice Helper function to construct voting module data for council elections + function _constructCouncilElectionVotingModuleData( + string[] memory descriptions, + uint128 criteriaValue + ) + internal + pure + returns (bytes memory) + { + // Construct ProposalOption array for elections (no execution calls) + ProposalOption[] memory options = new ProposalOption[](descriptions.length); + + for (uint256 i = 0; i < descriptions.length; i++) { + address[] memory targets = new address[](0); + uint256[] memory values = new uint256[](0); + bytes[] memory calldatas = new bytes[](0); + + options[i] = ProposalOption({ + budgetTokensSpent: 0, + targets: targets, + values: values, + calldatas: calldatas, + description: descriptions[i] + }); + } + + // Construct ProposalSettings with TopChoices criteria + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(descriptions.length), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), + criteriaValue: criteriaValue, + budgetAmount: 0 + }); + + return abi.encode(options, settings); + } + /// @notice Helper function to setup proposal types configurator mocks function _setupProposalTypesConfiguratorMocks() internal { // Mock calls for different proposal type IDs for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 2 || i == FUNDING_PROPOSALS_VOTING_MODULE) ? approvalVotingModule : address(0); + address moduleAddress = (i == 2 || i == APPROVAL_VOTING_MODULE_ID) ? approvalVotingModule : address(0); vm.mockCall( address(proposalTypesConfigurator), @@ -846,7 +903,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, "Test proposal", proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } @@ -1133,12 +1190,12 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - // Both GovernanceFund and CouncilBudget use FUNDING_PROPOSALS_VOTING_MODULE + // Both GovernanceFund and CouncilBudget use APPROVAL_VOTING_MODULE_ID if ( proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget ) { - assertEq(proposalVotingModule, FUNDING_PROPOSALS_VOTING_MODULE); + assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); } else { assertEq(proposalVotingModule, uint8(i)); } @@ -1184,3 +1241,227 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); } } + +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test +/// @notice Happy path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + string proposalDescription; + + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + + function setUp() public override { + super.setUp(); + + _setCouncilMemberElectionsProposalType(); + + proposalDescription = "Council Member Elections Q4 2024"; + } + + function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { + optionCount = uint8(bound(optionCount, 2, type(uint8).max)); // Minimum 2 options to have valid criteria < + // optionCount + criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount + + // Create dynamic array of option descriptions based on option count + string[] memory optionDescriptions = new string[](optionCount); + for (uint256 i = 0; i < optionCount; i++) { + optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); + } + + // Create attestation for the proposal + bytes32 attestationUid = + _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Expect ProposalSubmitted event + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections + ); + + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(topDelegate_A); + bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + (address proposer, ProposalValidator.ProposalType proposalType, bool inVoting, uint256 approvalCount) = + validator.getProposalData(proposalHash); + + assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); + assertEq( + uint8(proposalType), + uint8(ProposalValidator.ProposalType.CouncilMemberElections), + "Proposal type should be CouncilMemberElections" + ); + assertFalse(inVoting, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail +/// @notice Sad path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionDescriptions; + string proposalDescription; + bytes32 attestationUid; + + function setUp() public override { + super.setUp(); + + _setCouncilMemberElectionsProposalType(); + + criteriaValue = 2; + optionDescriptions = new string[](3); + optionDescriptions[0] = "Candidate A"; + optionDescriptions[1] = "Candidate B"; + optionDescriptions[2] = "Candidate C"; + + proposalDescription = "Test Council Elections"; + attestationUid = _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + } + + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) + public + { + vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } + + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { + string[] memory emptyOptions = new string[](0); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal(criteriaValue, emptyOptions, proposalDescription, attestationUid); + } + + function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + + // Create new attestation for second attempt + bytes32 secondAttestation = + _createAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(topDelegate_B); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, secondAttestation + ); + } + + function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( + uint128 invalidCriteriaValue + ) + public + { + // Bound invalidCriteriaValue to be greater than options length + invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } +} From 7d3152c4fe2e0ebbe3025ca5314ec3cf9d35e831 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:46:14 +0300 Subject: [PATCH 13/73] feat: approve proposal (#427) * fix: change attestation storage var name * feat: add approve proposal impl and tests * fix: pre-pr * fix: conflicts * chore: improve natspec * refactor: improve can approve --- .../governance/IProposalValidator.sol | 69 ++- .../snapshots/abi/ProposalValidator.json | 94 ++- .../storageLayout/ProposalValidator.json | 15 +- .../src/governance/ProposalValidator.sol | 136 +++-- .../test/governance/ProposalValidator.t.sol | 542 ++++++++++++------ 5 files changed, 519 insertions(+), 337 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index aa787840981..beca086f0c1 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -13,15 +13,16 @@ interface IProposalValidator is ISemver { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); - error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); - error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_VotingCycleAlreadySet(); + error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); + error ProposalValidator_AttestationRevoked(); + error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); struct ProposalData { @@ -36,7 +37,7 @@ interface IProposalValidator is ISemver { uint256 requiredApprovals; uint8 proposalVotingModule; } - + enum ProposalType { ProtocolOrGovernorUpgrade, MaintenanceUpgrade, @@ -45,6 +46,13 @@ interface IProposalValidator is ISemver { CouncilBudget } + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + string description, + ProposalType proposalType + ); + event ProposalApproved( bytes32 indexed proposalHash, address indexed approver @@ -55,9 +63,12 @@ interface IProposalValidator is ISemver { address indexed executor ); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + event VotingCycleDataSet( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -71,24 +82,12 @@ interface IProposalValidator is ISemver { bytes32 indexed proposalHash, bytes encodedVotingModuleData ); - - event VotingCycleDataSet( - uint256 cycleNumber, - uint256 startBlock, - uint256 duration, - uint256 votingCycleDistributionLimit - ); - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - string description, - ProposalType proposalType - ); - + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event Initialized(uint8 version); - function approveProposal(bytes32 _proposalHash) external; + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; function moveToVote( address[] memory _targets, @@ -96,8 +95,6 @@ interface IProposalValidator is ISemver { bytes[] memory _calldatas, string memory _description ) external returns (uint256 governorProposalId_); - - function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; @@ -105,7 +102,7 @@ interface IProposalValidator is ISemver { ProposalType _proposalType, ProposalTypeData memory _proposalTypeData ) external; - + function setVotingCycleData( uint256 _cycleNumber, uint256 _startBlock, @@ -132,7 +129,6 @@ interface IProposalValidator is ISemver { function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, - uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, uint256 _duration, @@ -141,14 +137,12 @@ interface IProposalValidator is ISemver { ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; - + function renounceOwnership() external; - - function canSignOff(address _delegate) external view returns (bool canSignOff_); - - function transferOwnership(address newOwner) external; - function minimumVotingPower() external view returns (uint256); + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + + function transferOwnership(address newOwner) external; function distributionThreshold() external view returns (uint256); @@ -160,20 +154,23 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); - + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( - uint256 startingBlock, - uint256 duration, + uint256 startingBlock, + uint256 duration, uint256 votingCycleDistributionLimit ); function __constructor__( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _votingToken ) external; diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index ec85035250f..958fb0af472 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -3,7 +3,12 @@ "inputs": [ { "internalType": "bytes32", - "name": "_attestationSchemaUid", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", "type": "bytes32" }, { @@ -22,7 +27,7 @@ }, { "inputs": [], - "name": "ATTESTATION_SCHEMA_UID", + "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", "outputs": [ { "internalType": "bytes32", @@ -46,6 +51,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "VOTING_TOKEN", @@ -65,6 +83,11 @@ "internalType": "bytes32", "name": "_proposalHash", "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" } ], "name": "approveProposal", @@ -74,17 +97,22 @@ }, { "inputs": [ + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + }, { "internalType": "address", "name": "_delegate", "type": "address" } ], - "name": "canSignOff", + "name": "canApproveProposal", "outputs": [ { "internalType": "bool", - "name": "canSignOff_", + "name": "canApprove_", "type": "bool" } ], @@ -129,11 +157,6 @@ "name": "_proposalTypesConfigurator", "type": "address" }, - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - }, { "internalType": "uint256", "name": "_cycleNumber", @@ -187,19 +210,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "minimumVotingPower", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -304,19 +314,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - } - ], - "name": "setMinimumVotingPower", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -534,19 +531,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newMinimumVotingPower", - "type": "uint256" - } - ], - "name": "MinimumVotingPowerSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -710,6 +694,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationRevoked", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ExceedsDistributionThreshold", @@ -722,12 +711,17 @@ }, { "inputs": [], - "name": "ProposalValidator_InsufficientVotingPower", + "name": "ProposalValidator_InvalidAttestation", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestation", + "name": "ProposalValidator_InvalidAttestationSchema", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidCriteriaValue", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 8e422ff0af2..50c94894f8b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -41,39 +41,32 @@ "slot": "101", "type": "contract IProposalTypesConfigurator" }, - { - "bytes": "32", - "label": "minimumVotingPower", - "offset": 0, - "slot": "102", - "type": "uint256" - }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "103", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "105", + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "105", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 91769149ae3..ed1fccc64a0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -37,9 +37,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when attempting to move a proposal to vote that is already in voting. error ProposalValidator_ProposalAlreadySubmitted(); - /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. - error ProposalValidator_InsufficientVotingPower(); - /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); @@ -61,6 +58,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). error ProposalValidator_InvalidOptionsLength(); + /// @notice Thrown when an attestation is revoked. + error ProposalValidator_AttestationRevoked(); + + /// @notice Thrown when an attestation schema is invalid. + error ProposalValidator_InvalidAttestationSchema(); + /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). error ProposalValidator_InvalidCriteriaValue(); @@ -142,10 +145,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param executor The address that executed the move to vote. event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); - /// @notice Emitted when the minimum voting power is set. - /// @param newMinimumVotingPower The new minimum voting power. - event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. /// @param startBlock The block number of the starting block of the voting cycle. @@ -170,22 +169,24 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param encodedVotingModuleData The encoded voting module data. event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - /// @notice The schema UID for attestations in the Ethereum Attestation Service. + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller + /// is an approved proposer. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } - bytes32 public immutable ATTESTATION_SCHEMA_UID; + bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller + /// is part of the top100 delegates. + bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The token used to determine voting power. + /// @notice The governance token contract. IGovernanceToken public immutable VOTING_TOKEN; /// @notice The proposal types configurator contract. IProposalTypesConfigurator public proposalTypesConfigurator; - /// @notice The minimum voting power required for a delegate to approve proposals. - uint256 public minimumVotingPower; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; @@ -205,17 +206,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Constructs the ProposalValidator contract. - /// @param _attestationSchemaUid The schema UID for attestations in EAS. + /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. + /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller + /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. /// @param _votingToken The token used to determine voting power. constructor( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _votingToken ) ReinitializableBase(1) { - ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; GOVERNOR = _governor; VOTING_TOKEN = _votingToken; _disableInitializers(); @@ -224,7 +229,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _proposalTypesConfigurator The proposal types configurator contract address. - /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _cycleNumber The number of the current voting cycle. /// @param _startBlock The block number of the starting block of the voting cycle. /// @param _duration The duration of the voting cycle. @@ -235,7 +239,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, - uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, uint256 _duration, @@ -252,7 +255,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposalTypesConfigurator = _proposalTypesConfigurator; - _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); @@ -281,7 +283,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Validate EAS attestation - must be called by owner-approved address - _validateAttestation(_attestationUid, ProposalType.CouncilMemberElections); + _validateApprovedProposerAttestation(_attestationUid, ProposalType.CouncilMemberElections); // Validate options length bounds uint256 optionsLength = _optionDescriptions.length; @@ -456,23 +458,31 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power) + /// @notice Approves a proposal before being moved for voting. + /// @dev This function should only be called by the top delegates. /// @param _proposalHash The hash of the proposal to approve - function approveProposal(bytes32 _proposalHash) external { - if (!canSignOff(msg.sender)) { - revert ProposalValidator_InsufficientVotingPower(); - } - + /// @param _attestationUid The UID of the attestation for the delegate to approve the proposal + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external { + address _delegate = _msgSender(); ProposalData storage proposal = _proposals[_proposalHash]; + // check if the proposal exists + if (proposal.proposer == address(0)) { + revert ProposalValidator_ProposalDoesNotExist(); + } - if (proposal.delegateApprovals[msg.sender]) { + // check if the caller has already approved the proposal + if (proposal.delegateApprovals[_delegate]) { revert ProposalValidator_ProposalAlreadyApproved(); } - proposal.delegateApprovals[msg.sender] = true; + // validate the attestation + _validateTopDelegateAttestation(_attestationUid, _msgSender()); + + // store the approval + proposal.delegateApprovals[_delegate] = true; proposal.approvalCount++; - emit ProposalApproved(_proposalHash, msg.sender); + emit ProposalApproved(_proposalHash, _delegate); } /// @notice Move a proposal to voting phase after sufficient delegate approvals @@ -516,17 +526,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalMovedToVote(_proposalHash, msg.sender); } - /// @notice Returns whether a delegate has enough voting power to approve a proposal. - /// @param _delegate The address of the delegate to check. - /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. - function canSignOff(address _delegate) public view returns (bool canSignOff_) { - canSignOff_ = VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; - } - - /// @notice Sets the minimum voting power required for a delegate to approve proposals. - /// @param _minimumVotingPower The new minimum voting power threshold. - function setMinimumVotingPower(uint256 _minimumVotingPower) external onlyOwner { - _setMinimumVotingPower(_minimumVotingPower); + /// @notice Checks if a delegate can approve a proposal. + /// @dev Helper function for UI integration. + /// @param _attestationUid The UID of the attestation to check. + /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); } /// @notice Sets the data of a voting cycle. @@ -571,7 +576,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// Reverts with ProposalValidator_InvalidAttestation if validation fails. /// @param _attestationUid The UID of the attestation to validate. /// @param _expectedProposalType The expected proposal type from the attestation. - function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { + function _validateApprovedProposerAttestation( + bytes32 _attestationUid, + ProposalType _expectedProposalType + ) + internal + view + { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) @@ -582,13 +593,47 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( - attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } } + /// @notice Validates the attestation data for a delegate that tries to approve a proposal. + /// @dev Only acceptes attestations that does NOT include partial delegation. + /// @param _attestationUid The UID of the attestation to validate. + /// @param _delegate The delegate to validate the attestation for. + /// @return canApprove_ True if the attestation is valid, false otherwise. + function _validateTopDelegateAttestation( + bytes32 _attestationUid, + address _delegate + ) + internal + view + returns (bool canApprove_) + { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + + // check if the schema is correct + if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { + revert ProposalValidator_InvalidAttestationSchema(); + } + + // check if the attestation is revoked + if (attestation.revocationTime != 0) { + revert ProposalValidator_AttestationRevoked(); + } + + // check if the attestation includes partial delegation or the recipient is not the caller + if (_includePartialDelegation || attestation.recipient != _delegate) { + revert ProposalValidator_InvalidAttestation(); + } + + canApprove_ = true; + } + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. @@ -606,13 +651,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); } - /// @notice Private function to set the minimum voting power and emit event. - /// @param _minimumVotingPower The new minimum voting power threshold. - function _setMinimumVotingPower(uint256 _minimumVotingPower) private { - minimumVotingPower = _minimumVotingPower; - emit MinimumVotingPowerSet(_minimumVotingPower); - } - /// @notice Private function to set the voting cycle data and emit event. /// @param _cycleNumber The number of the voting cycle to set. /// @param _startBlock The block number of the starting block of the voting cycle. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 12593e24e7b..bd4cc5aa594 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -6,7 +6,13 @@ import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; -import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; +import { + IEAS, + AttestationRequest, + AttestationRequestData, + RevocationRequest, + RevocationRequestData +} from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -32,11 +38,17 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @notice A test contract that exposes the private _hashProposal function contract ProposalValidatorForTest is ProposalValidator { constructor( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _governanceToken ) - ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) + ProposalValidator( + _approvedProposerAttestationSchemaUid, + _topDelegatesAttestationSchemaUid, + _governor, + _governanceToken + ) { } function hashProposalWithModule( @@ -61,6 +73,25 @@ contract ProposalValidatorForTest is ProposalValidator { return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); } + function setProposalData( + bytes32 _proposalHash, + address _proposer, + ProposalType _proposalType, + bool _inVoting, + uint256 _approvalCount + ) + public + { + _proposals[_proposalHash].proposer = _proposer; + _proposals[_proposalHash].proposalType = _proposalType; + _proposals[_proposalHash].inVoting = _inVoting; + _proposals[_proposalHash].approvalCount = _approvalCount; + } + + function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { + _proposals[_proposalHash].delegateApprovals[_delegate] = true; + } + /// @notice Check if a delegate has approved a proposal function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { return _proposals[_proposalHash].delegateApprovals[_delegate]; @@ -72,29 +103,32 @@ contract ProposalValidatorForTest is ProposalValidator { contract ProposalValidator_Init is CommonTest { using stdStorage for StdStorage; - uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; uint256 public constant DURATION = 100; uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; - uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; address owner; address user; - address topDelegate_A; - address topDelegate_B; - address topDelegate_C; - address topDelegate_D; + address topDelegate_A = makeAddr("topDelegate_A"); + address topDelegate_B = makeAddr("topDelegate_B"); + address topDelegate_C = makeAddr("topDelegate_C"); + address topDelegate_D = makeAddr("topDelegate_D"); + bytes32 topDelegateAttestation_A; + bytes32 topDelegateAttestation_B; + bytes32 topDelegateAttestation_C; + bytes32 topDelegateAttestation_D; address approvalVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; IOptimismGovernor public governor; IProposalTypesConfigurator public proposalTypesConfigurator; - bytes32 public ATTESTATION_SCHEMA_UID; + bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; event ProposalSubmitted( @@ -120,21 +154,6 @@ contract ProposalValidator_Init is CommonTest { vm.expectCall(_receiver, _calldata); } - /// @notice Helper function to make a top delegate. - function _makeTopDelegate(string memory _name) internal returns (address) { - address delegate = makeAddr(_name); - deal(address(governanceToken), delegate, TOP_DELEGATE_VOTING_POWER); - vm.prank(delegate); - governanceToken.delegate(delegate); - return delegate; - } - - /// @notice Helper function to make a (top) delegate approve a proposal. - function _approveProposal(address _delegate, bytes32 _proposalHash) internal { - vm.prank(_delegate); - validator.approveProposal(_proposalHash); - } - /// @notice Helper function to set proposal type data using StdStorage. function _setProposalTypeData( ProposalValidator.ProposalType _proposalType, @@ -190,6 +209,7 @@ contract ProposalValidator_Init is CommonTest { _setCouncilBudgetProposalType(); } /// @notice Helper to create minimal valid arrays for funding proposal error tests + function _createMinimalFundingArrays() internal pure @@ -363,7 +383,9 @@ contract ProposalValidator_Init is CommonTest { // Setup mocks _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -374,7 +396,6 @@ contract ProposalValidator_Init is CommonTest { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -395,21 +416,28 @@ contract ProposalValidator_Init is CommonTest { governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); + // Create schemas vm.prank(owner); - ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false ); + vm.prank(owner); + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100,bool includePartialDelegation,string date", ISchemaResolver(address(0)), true + ); + _initializeValidator(); - topDelegate_A = _makeTopDelegate("topDelegate_A"); - topDelegate_B = _makeTopDelegate("topDelegate_B"); - topDelegate_C = _makeTopDelegate("topDelegate_C"); - topDelegate_D = _makeTopDelegate("topDelegate_D"); + // Create attestations for top delegates + topDelegateAttestation_A = _createTopDelegateAttestation(topDelegate_A); + topDelegateAttestation_B = _createTopDelegateAttestation(topDelegate_B); + topDelegateAttestation_C = _createTopDelegateAttestation(topDelegate_C); + topDelegateAttestation_D = _createTopDelegateAttestation(topDelegate_D); } - /// @notice Helper to create a valid attestation for a proposal - function _createAttestation( + /// @notice Helper to create a valid attestation for an approved proposer + function _createApprovedProposerAttestation( address _delegate, ProposalValidator.ProposalType _proposalType ) @@ -419,7 +447,7 @@ contract ProposalValidator_Init is CommonTest { vm.prank(owner); return IEAS(Predeploys.EAS).attest( AttestationRequest({ - schema: ATTESTATION_SCHEMA_UID, + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ recipient: address(0), expirationTime: 0, @@ -432,6 +460,24 @@ contract ProposalValidator_Init is CommonTest { ); } + /// @notice Helper to create a valid attestation for a top delegate + function _createTopDelegateAttestation(address _delegate) internal returns (bytes32) { + vm.prank(owner); + return IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: _delegate, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); + } + /// @notice Helper to create a standard proposal setup function _createProposalSetup() internal @@ -456,40 +502,30 @@ contract ProposalValidator_Init is CommonTest { /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - function setUp() public override { - super.setUp(); + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Ensure the proposal hash is not 0 + vm.assume(_proposalHash != bytes32(0)); - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - } + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - function test_approveProposal_succeeds() public { // Expect event to be emitted when approving vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_A); - _approveProposal(topDelegate_A, proposalHash); + emit ProposalApproved(_proposalHash, topDelegate_A); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_B); - _approveProposal(topDelegate_B, proposalHash); + // Approve the proposal, use the attestation of the top delegate that was created in setUp + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_C); - _approveProposal(topDelegate_C, proposalHash); + // Check that the proposal data has been updated + assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_D); - _approveProposal(topDelegate_D, proposalHash); + (,,, uint256 approvalCount) = validator.getProposalData(_proposalHash); + assertEq(approvalCount, 1); } } @@ -498,165 +534,287 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { function setUp() public override { super.setUp(); - - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } - function test_approveProposal_insufficientVotingPower_reverts() public { - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(user, proposalHash); + function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + // There is no stored proposal data so this will revert + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function test_approveProposal_alreadyApproved_reverts() public { - _approveProposal(topDelegate_A, proposalHash); + function test_approveProposal_proposalAlreadyApproved_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // Mock the proposal as already approved by the top delegate + validator.mockApproveProposal(_proposalHash, topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - _approveProposal(topDelegate_A, proposalHash); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} - -/// @title ProposalValidator_MoveToVote_Test -/// @notice Happy path tests for moveToVote function -contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; - ProposalValidator.ProposalType proposalType; - uint8 proposalVotingModule; - function setUp() public override { - super.setUp(); + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - (targets, values, calldatas, description) = _createProposalSetup(); + // create a new schema + vm.prank(topDelegate_A); + bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100, string date", ISchemaResolver(address(0)), true + ); - proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // create an attestation with the new schema + vm.prank(topDelegate_A); + bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: _invalidSchemaUid, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); - _approveProposal(topDelegate_D, proposalHash); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _invalidAttestationUid); } - function test_moveToVote_succeeds() public { - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); - - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(proposalHash, owner); + function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // revoke the attestation vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) + }) + ); - assertEq(governorProposalId, 1); + vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} -/// @title ProposalValidator_MoveToVote_TestFail -/// @notice Sad path tests for moveToVote function -contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; - ProposalValidator.ProposalType proposalType; - uint8 proposalVotingModule; - - function setUp() public override { - super.setUp(); - - (targets, values, calldatas, description) = _createProposalSetup(); - - proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + function test_approveProposal_invalidAttestationCaller_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + address _caller + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - } + // Ensure the caller is not a top delegate + vm.assume( + _caller != topDelegate_A && _caller != topDelegate_B && _caller != topDelegate_C && _caller != topDelegate_D + ); - function test_moveToVote_insufficientApprovals_reverts() public { - // Only approve with 3 delegates (need 4) - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(_caller); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function test_moveToVote_alreadyProposed_reverts() public { - // Approve with all 4 delegates - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); - _approveProposal(topDelegate_D, proposalHash); + function test_approveProposal_invalidAttestationPartialDelegation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // create an attestation with partial delegation vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", true, "2000-01-01"), + value: 0 + }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); } } -/// @title ProposalValidator_Getters_Test -/// @notice Tests for getter functions -contract ProposalValidator_Getters_Test is ProposalValidator_Init { - function test_canSignOff_succeeds() public { - bool canSignOff = validator.canSignOff(topDelegate_A); - assertTrue(canSignOff); +// /// @title ProposalValidator_MoveToVote_Test +// /// @notice Happy path tests for moveToVote function +// contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { +// address[] targets; +// uint256[] values; +// bytes[] calldatas; +// string description; +// ProposalValidator.ProposalType proposalType; +// uint8 proposalVotingModule; + +// function setUp() public override { +// super.setUp(); + +// (targets, values, calldatas, description) = _createProposalSetup(); + +// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; +// proposalVotingModule = 0; +// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + +// /* vm.prank(topDelegate_A); */ +// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented + +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); +// _approveProposal(topDelegate_D, proposalHash); +// } + +// function test_moveToVote_succeeds() public { +// _mockAndExpect( +// address(governor), +// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, +// proposalVotingModule)), +// abi.encode(1) +// ); + +// // Expect the ProposalMovedToVote event to be emitted +// vm.expectEmit(address(validator)); +// emit ProposalMovedToVote(proposalHash, owner); + +// vm.prank(owner); +// uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); + +// assertEq(governorProposalId, 1); +// } +// } + +// /// @title ProposalValidator_MoveToVote_TestFail +// /// @notice Sad path tests for moveToVote function +// contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { +// address[] targets; +// uint256[] values; +// bytes[] calldatas; +// string description; +// ProposalValidator.ProposalType proposalType; +// uint8 proposalVotingModule; + +// function setUp() public override { +// super.setUp(); + +// (targets, values, calldatas, description) = _createProposalSetup(); + +// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; +// proposalVotingModule = 0; +// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + +// /* vm.prank(topDelegate_A); */ +// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented +// } + +// function test_moveToVote_insufficientApprovals_reverts() public { +// // Only approve with 3 delegates (need 4) +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); + +// vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); +// } + +// function test_moveToVote_alreadyProposed_reverts() public { +// // Approve with all 4 delegates +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); +// _approveProposal(topDelegate_D, proposalHash); + +// _mockAndExpect( +// address(governor), +// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, +// proposalVotingModule)), +// abi.encode(1) +// ); + +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); + +// vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); +// } +// } + +/// @title ProposalValidator_CanApproveProposal_Test +/// @notice Tests for the canApproveProposal function +contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { + function test_canApproveProposal_ReturnsTrue_succeeds() public { + // Attestation already created in setUp + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + assertTrue(canApprove); + } + + function test_canApproveProposal_ReturnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + // Ensure the attestation uid is not one of the top delegates + vm.assume( + _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B + && _attestationUid != topDelegateAttestation_C && _attestationUid != topDelegateAttestation_D + ); - bool cannotSignOff = validator.canSignOff(user); - assertFalse(cannotSignOff); + bool canApprove; + // Expect the invalid attestation error to be reverted + vm.expectRevert(); + try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result) { + canApprove = result; + } catch { + canApprove = false; + } + + assertEq(canApprove, false); } } /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { - function testFuzz_setMinimumVotingPower_succeeds(uint256 newMinimumVotingPower) public { - // Expect the MinimumVotingPowerSet event to be emitted - vm.expectEmit(address(validator)); - emit MinimumVotingPowerSet(newMinimumVotingPower); - - vm.prank(owner); - validator.setMinimumVotingPower(newMinimumVotingPower); - - assertEq(validator.minimumVotingPower(), newMinimumVotingPower); - } - - function test_setMinimumVotingPower_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setMinimumVotingPower(10000 ether); - } - function testFuzz_setVotingCycleData_succeeds( uint256 cycleNumber, uint256 startBlock, @@ -1143,7 +1301,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Setup mocks _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); // Initialize will be tested manually } @@ -1162,7 +1322,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -1175,7 +1334,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); // Verify initialization was successful - assertEq(validator.minimumVotingPower(), MINIMUM_VOTING_POWER); assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); @@ -1228,7 +1386,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -1270,7 +1427,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal // Create attestation for the proposal bytes32 attestationUid = - _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); // Calculate expected proposal hash bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); @@ -1337,7 +1494,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop optionDescriptions[2] = "Candidate C"; proposalDescription = "Test Council Elections"; - attestationUid = _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) @@ -1393,7 +1551,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Create new attestation for second attempt bytes32 secondAttestation = - _createAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + _createApprovedProposerAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); // Attempt to submit identical proposal should revert vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); @@ -1424,14 +1582,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } - function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) + public + { vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner // Create attestation but don't use proper owner as attester vm.prank(fuzzedAttester); // Not the owner bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( AttestationRequest({ - schema: ATTESTATION_SCHEMA_UID, + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ recipient: address(0), expirationTime: 0, From a87e8ccb6179904a2c3d010ee0aad763d000efa2 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:13:40 -0300 Subject: [PATCH 14/73] feat: add submit upgrade proposal (#429) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * feat: add submitCouncilMemberElectionsProposal function * chore: run pre-pr * fix: remove duplicated tests * test(fuzz): use fuzz testing for happy paths * feat: add check for criteria value < optionslength * perf: optimiza for loops usage * test: fuzz invalid attestationUid * test: fuzz invalid proposer * fix: lack of attestation existance validation * test: fuzz exceeded max options test * test: fuzz unapproved attester * test: reduce upper bound for array size * test: remove duplicated test * fix: update criteria value validation logic in ProposalValidator * refactor(test): use helper functions for duplicated logic * refactor: declare approvalVotingModule variable * test: increment the assertions in council memeber election tests * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr * refactor: use variables instead of hardcoded values * chore: rename voting module depending on configurator instead of internal proposal type * chore: improve tests naming * test: expect proposal type configurator calls * feat: add submitUpgradeProposal function * refactor: improve variable names consistency * test: add submitUpgradeProposal tests * chore: run pre-pr * refactpr(test): separate submitUpgradePropowal tests depending on proposal type * test: improve coverage on InvalidProposalType tests * chore: improve code legibility * chore: improve comments Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * fix: merge conflicts * fix: broken compile for missing variable * fix: broken tests out of enum bounds * fix: pre-pr * fix: correct order for event emission * refactor(test): declare events in Init contract * refactor: use constants for code legibility --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .semgrep/rules/sol-rules.yaml | 3 + .../governance/IOptimismGovernor.sol | 12 + .../governance/IProposalValidator.sol | 17 +- .../snapshots/abi/OptimisticModule.json | 258 ++++++++++ .../snapshots/abi/ProposalValidator.json | 45 +- .../snapshots/semver-lock.json | 4 +- .../storageLayout/OptimisticModule.json | 9 + .../src/governance/OptimisticModule.sol | 154 ++++++ .../src/governance/ProposalValidator.sol | 104 +++- .../test/governance/ProposalValidator.t.sol | 457 ++++++++++++++++-- 10 files changed, 1015 insertions(+), 48 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/OptimisticModule.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json create mode 100644 packages/contracts-bedrock/src/governance/OptimisticModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index 9cf01c69ce1..3d24d5e8fae 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -129,6 +129,7 @@ rules: - packages/contracts-bedrock/src/governance/GovernanceToken.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -155,6 +156,7 @@ rules: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -255,6 +257,7 @@ rules: - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index 39dd12b664a..994bb597df4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {VotingModule} from "src/governance/VotingModule.sol"; +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; interface IOptimismGovernor { function propose( @@ -21,6 +22,17 @@ interface IOptimismGovernor { function timelock() external view returns (address); + function PROPOSAL_TYPES_CONFIGURATOR() external view returns (address); + + function token() external view returns (IVotesUpgradeable); + + function getProposalType(uint256 proposalId) external view returns (uint8); + + function proposalVotes(uint256 proposalId) + external + view + returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); + /// @notice Returns the snapshot block number for a proposal, 0 if proposal doesn't exist /// @param proposalId The ID of the proposal /// @return The snapshot block number, or 0 if proposal doesn't exist diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index beca086f0c1..815b321a5df 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -24,6 +24,8 @@ interface IProposalValidator is ISemver { error ProposalValidator_AttestationRevoked(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); + error ProposalValidator_InvalidUpgradeProposalType(); + error ProposalValidator_InvalidAgainstThreshold(); struct ProposalData { address proposer; @@ -83,10 +85,10 @@ interface IProposalValidator is ISemver { bytes encodedVotingModuleData ); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event Initialized(uint8 version); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; function moveToVote( @@ -126,6 +128,13 @@ interface IProposalValidator is ISemver { ProposalType _proposalType ) external returns (bytes32 proposalHash_); + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType + ) external returns (bytes32 proposalHash_); + function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, @@ -140,10 +149,10 @@ interface IProposalValidator is ISemver { function renounceOwnership() external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); - function transferOwnership(address newOwner) external; + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + function distributionThreshold() external view returns (uint256); function VOTING_TOKEN() external view returns (IGovernanceToken); diff --git a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json new file mode 100644 index 00000000000..f2e29a066bd --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json @@ -0,0 +1,258 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_governor", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "COUNTING_MODE", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "PERCENT_DIVISOR", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PROPOSAL_DATA_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "VOTE_PARAMS_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "_countVote", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "_formatExecuteParams", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "", + "type": "bytes[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "_voteSucceeded", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "proposals", + "outputs": [ + { + "internalType": "address", + "name": "governor", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint248", + "name": "againstThreshold", + "type": "uint248" + }, + { + "internalType": "bool", + "name": "isRelativeToVotableSupply", + "type": "bool" + } + ], + "internalType": "struct ProposalSettings", + "name": "settings", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_proposalData", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "_descriptionHash", + "type": "bytes32" + } + ], + "name": "propose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ExistingProposal", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParams", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernor", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_NotOptimisticProposalType", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_OptimisticModuleOnlySignal", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_WrongProposalId", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 958fb0af472..9b982f52282 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -450,6 +450,40 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint248", + "name": "_againstThreshold", + "type": "uint248" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "submitUpgradeProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -711,17 +745,17 @@ }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestation", + "name": "ProposalValidator_InvalidAgainstThreshold", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestationSchema", + "name": "ProposalValidator_InvalidAttestation", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidCriteriaValue", + "name": "ProposalValidator_InvalidAttestationSchema", "type": "error" }, { @@ -739,6 +773,11 @@ "name": "ProposalValidator_InvalidOptionsLength", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidUpgradeProposalType", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 8ce0da8c95e..f5ed5805cf8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xe5a13d76f05b05db718ac645ee1aaf8770de307b8f4e367e3def458d1be6ba3f", - "sourceCodeHash": "0x2e2655c25888e6d3502374d7e257888ecc300188efbc18c44b19fc6640b00629" + "initCodeHash": "0x5c805e6dba872f7d2f16cb27a1ab7f8b31813e2b79b04d5ea61055bd06f0bf74", + "sourceCodeHash": "0xc1b29b287e1ff7df81aa3bc740fe63fc0203a976d222b1241636b639a478b791" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json new file mode 100644 index 00000000000..a600d98d300 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "32", + "label": "proposals", + "offset": 0, + "slot": "0", + "type": "mapping(uint256 => struct Proposal)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/OptimisticModule.sol b/packages/contracts-bedrock/src/governance/OptimisticModule.sol new file mode 100644 index 00000000000..736b73cbb57 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/OptimisticModule.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IGovernorUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol"; +import { IVotesUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { VotingModule } from "./VotingModule.sol"; + +enum VoteType { + Against, + For, + Abstain +} + +struct ProposalSettings { + uint248 againstThreshold; + bool isRelativeToVotableSupply; +} + +struct Proposal { + address governor; + ProposalSettings settings; +} + +contract OptimisticModule is VotingModule { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error OptimisticModule_WrongProposalId(); + error OptimisticModule_NotOptimisticProposalType(); + error OptimisticModule_OptimisticModuleOnlySignal(); + + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + uint16 public constant PERCENT_DIVISOR = 10_000; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => Proposal) public proposals; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) VotingModule(_governor) { } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Validate proposal is optimistic and save settings for a new proposal. + /// @param _proposalId The id of the proposal. + /// @param _proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. + function propose(uint256 _proposalId, bytes memory _proposalData, bytes32 _descriptionHash) external override { + _onlyGovernor(); + if (_proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), _proposalData, _descriptionHash)))) { + revert OptimisticModule_WrongProposalId(); + } + + if (proposals[_proposalId].governor != address(0)) { + revert ExistingProposal(); + } + + ProposalSettings memory proposalSettings = abi.decode(_proposalData, (ProposalSettings)); + + uint8 proposalTypeId = IOptimismGovernor(msg.sender).getProposalType(_proposalId); + IProposalTypesConfigurator proposalConfigurator = + IProposalTypesConfigurator(IOptimismGovernor(msg.sender).PROPOSAL_TYPES_CONFIGURATOR()); + IProposalTypesConfigurator.ProposalType memory proposalType = proposalConfigurator.proposalTypes(proposalTypeId); + + if (proposalType.quorum != 0 || proposalType.approvalThreshold != 0) { + revert OptimisticModule_NotOptimisticProposalType(); + } + if ( + proposalSettings.againstThreshold == 0 + || (proposalSettings.isRelativeToVotableSupply && proposalSettings.againstThreshold > PERCENT_DIVISOR) + ) { + revert InvalidParams(); + } + + proposals[_proposalId].governor = msg.sender; + proposals[_proposalId].settings = proposalSettings; + } + + /// @notice Counting logic is skipped. + function _countVote(uint256, address, uint8, uint256, bytes memory) external virtual override { } + + /// @notice Reverts to prevent queue and execute of proposals with optimistic module. + function _formatExecuteParams( + uint256, + bytes memory + ) + public + pure + override + returns (address[] memory, uint256[] memory, bytes[] memory) + { + revert OptimisticModule_OptimisticModuleOnlySignal(); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Return true if `againstVotes` is lower than `againstThreshold`. + /// Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. + /// @param _proposalId The id of the proposal. + function _voteSucceeded(uint256 _proposalId) external view override returns (bool) { + Proposal memory proposal = proposals[_proposalId]; + (uint256 againstVotes,,) = IOptimismGovernor(proposal.governor).proposalVotes(_proposalId); + + uint256 againstThreshold = proposal.settings.againstThreshold; + if (proposal.settings.isRelativeToVotableSupply) { + uint256 snapshotBlock = IGovernorUpgradeable(proposal.governor).proposalSnapshot(_proposalId); + IVotesUpgradeable token = IOptimismGovernor(proposal.governor).token(); + againstThreshold = (token.getPastTotalSupply(snapshotBlock) * againstThreshold) / PERCENT_DIVISOR; + } + + return againstVotes < againstThreshold; + } + + /// @dev Defines the encoding for the expected `proposalData` in `propose`. + /// Encoding: `(ProposalSettings)` + /// Can be used by clients to interact with modules programmatically without prior knowledge + /// on expected types. + function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { + return "((uint248 againstThreshold,bool isRelativeToVotableSupply) proposalSettings)"; + } + + /// @dev Defines the encoding for the expected `params` in `_countVote`. + /// Can be used by clients to interact with modules programmatically without prior knowledge + /// on expected types. + function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { + return ""; + } + + /// @dev See {IGovernor-COUNTING_MODE}. + /// - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. + /// - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo&quorum=against,for,abstain"; + } + + /// @notice Module version. + function version() public pure returns (uint256) { + return 1; + } +} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index ed1fccc64a0..3f3146f00d6 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -17,7 +17,13 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; // Modules -import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; +import { + ProposalSettings as ApprovalProposalSettings, + ProposalOption, + PassingCriteria +} from "src/governance/ApprovalVotingModule.sol"; +import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; +import { VotingModule } from "src/governance/VotingModule.sol"; /// @custom:proxied true /// @title ProposalValidator @@ -67,6 +73,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). error ProposalValidator_InvalidCriteriaValue(); + /// @notice Thrown when the against threshold is invalid (must be > 0 and <= 10000 basis points). + error ProposalValidator_InvalidAgainstThreshold(); + + /// @notice Thrown when an invalid proposal type is provided for upgrade proposals. + error ProposalValidator_InvalidUpgradeProposalType(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -122,6 +134,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { CouncilBudget } + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice The divisor used for percentage calculations in optimistic voting modules. + /// @dev Represents 100% in basis points (10,000 = 100%). + uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -266,6 +286,84 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a Protocol/Governor Upgrade or Maintenance Upgrade proposal. + /// @param _againstThreshold The percentage that will be used to calculate the fraction of the votable supply + /// that the proposal will need in votes against it to fail. + /// @param _proposalDescription Description of the proposal. + /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). + /// @return proposalHash_ The hash of the submitted proposal. + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + // Validate proposal type is valid for upgrade proposals + if (_proposalType != ProposalType.ProtocolOrGovernorUpgrade && _proposalType != ProposalType.MaintenanceUpgrade) + { + revert ProposalValidator_InvalidUpgradeProposalType(); + } + + // Validate EAS attestation - must be called by owner-approved address + _validateApprovedProposerAttestation(_attestationUid, _proposalType); + + // Validate againstThreshold is non-zero and within bounds for percentage-based thresholds + if (_againstThreshold == 0 || _againstThreshold > OPTIMISTIC_MODULE_PERCENT_DIVISOR) { + revert ProposalValidator_InvalidAgainstThreshold(); + } + + // Create OptimisticModule ProposalSettings with required parameters + OptimisticProposalSettings memory optimisticSettings = OptimisticProposalSettings({ + againstThreshold: _againstThreshold, + isRelativeToVotableSupply: true // MUST always be true + }); + + // Optimistic proposals are signal-only, no execution targets/calldatas needed + bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); + + // Get the optimistic module address from configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = _proposalType; + + emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + + // MaintenanceUpgrade proposals move directly to voting (atomic operation) + if (_proposalType == ProposalType.MaintenanceUpgrade) { + proposal.inVoting = true; + + GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + ); + + emit ProposalMovedToVote(proposalHash_, msg.sender); + } + } + /// @notice Submits a Council Member Elections proposal for approval and voting. /// @param _criteriaValue Since the passing criteria type is "TopChoices" this number represents the amount /// of top choices that can pass the voting. @@ -314,7 +412,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval voting settings with TopChoices criteria - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections @@ -421,7 +519,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval voting settings - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index bd4cc5aa594..ede38a009b1 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -28,7 +28,13 @@ import { Proxy } from "src/universal/Proxy.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Modules -import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; +import { + ProposalSettings as ApprovalProposalSettings, + ProposalOption, + PassingCriteria +} from "src/governance/ApprovalVotingModule.sol"; +import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; +import { VotingModule } from "src/governance/VotingModule.sol"; // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; @@ -109,7 +115,10 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; - uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; + uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; + uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; + uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; address owner; address user; @@ -122,6 +131,7 @@ contract ProposalValidator_Init is CommonTest { bytes32 topDelegateAttestation_C; bytes32 topDelegateAttestation_D; address approvalVotingModule; + address optimisticVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; @@ -147,6 +157,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -208,6 +219,34 @@ contract ProposalValidator_Init is CommonTest { _setGovernanceFundProposalType(); _setCouncilBudgetProposalType(); } + + /// @notice Helper function to set ProtocolOrGovernorUpgrade proposal type data. + function _setProtocolOrGovernorUpgradeProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + }) + ); + } + + /// @notice Helper function to set MaintenanceUpgrade proposal type data. + function _setMaintenanceUpgradeProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.MaintenanceUpgrade, + ProposalValidator.ProposalTypeData({ + requiredApprovals: 0, // MaintenanceUpgrade moves directly to voting + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + }) + ); + } + + /// @notice Helper function to set both upgrade proposal types. + function _setUpgradeProposalTypes() internal { + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + } /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() @@ -299,7 +338,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -337,7 +376,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(PassingCriteria.TopChoices), budgetToken: address(0), @@ -348,26 +387,36 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(options, settings); } + /// @notice Helper function to construct voting module data for upgrade proposals + function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { + OptimisticProposalSettings memory settings = + OptimisticProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); + + return abi.encode(settings); + } + /// @notice Helper function to setup proposal types configurator mocks - function _setupProposalTypesConfiguratorMocks() internal { - // Mock calls for different proposal type IDs - for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 2 || i == APPROVAL_VOTING_MODULE_ID) ? approvalVotingModule : address(0); - - vm.mockCall( - address(proposalTypesConfigurator), - abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (i)), - abi.encode( - IProposalTypesConfigurator.ProposalType({ - quorum: 100, - approvalThreshold: 100, - name: "Test Proposal Type", - description: "Test Description", - module: moduleAddress - }) - ) - ); + function _mockProposalTypesConfiguratorCall(uint8 _votingModuleId) internal { + address moduleAddress; + if (_votingModuleId == APPROVAL_VOTING_MODULE_ID) { + moduleAddress = approvalVotingModule; + } else if (_votingModuleId == OPTIMISTIC_VOTING_MODULE_ID) { + moduleAddress = optimisticVotingModule; } + + _mockAndExpect( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 100, + approvalThreshold: 100, + name: "Test Proposal Type", + description: "Test Description", + module: moduleAddress + }) + ) + ); } /// @notice Initializes the validator @@ -380,9 +429,6 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Setup mocks - _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest( APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken ); @@ -415,6 +461,7 @@ contract ProposalValidator_Init is CommonTest { user = makeAddr("user"); governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); + optimisticVotingModule = makeAddr("optimisticVotingModule"); // Create schemas vm.prank(owner); @@ -786,13 +833,13 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_ReturnsTrue_succeeds() public { + function test_canApproveProposal_returnsTrue_succeeds() public { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); } - function test_canApproveProposal_ReturnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + function test_canApproveProposal_returnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { // Ensure the attestation uid is not one of the top delegates vm.assume( _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B @@ -955,8 +1002,6 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init uint256[] optionsAmounts; string description; - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - function setUp() public override { super.setUp(); @@ -1016,6 +1061,8 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedHash, votingModuleData); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(proposer); bytes32 proposalHash = validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); @@ -1050,7 +1097,6 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { - // Bound to proposal types that are NOT funding proposals (0, 1, 2) // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -1210,6 +1256,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I abi.encode(0) ); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Submit first proposal vm.prank(user); validator.submitFundingProposal( @@ -1218,6 +1266,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Attempt to submit identical proposal vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(user); validator.submitFundingProposal( FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType @@ -1246,6 +1297,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(user); validator.submitFundingProposal( FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType @@ -1298,14 +1352,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Setup mocks - _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest( APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken ); validator = ProposalValidatorForTest(address(new Proxy(owner))); - // Initialize will be tested manually } function test_initialize_succeeds() public { @@ -1404,8 +1454,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { string proposalDescription; - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - function setUp() public override { super.setUp(); @@ -1452,6 +1500,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedHash, votingModuleData); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid @@ -1543,6 +1593,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop abi.encode(0) ); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Submit first proposal vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( @@ -1555,6 +1607,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Attempt to submit identical proposal should revert vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_B); validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, secondAttestation @@ -1576,6 +1631,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid @@ -1625,3 +1683,330 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } } + +/// @title ProposalValidator_SubmitUpgradeProposal_Test +/// @notice Happy path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { + string proposalDescription; + + function setUp() public override { + super.setUp(); + + _setUpgradeProposalTypes(); + + proposalDescription = "Protocol Upgrade Proposal"; + } + + function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( + uint248 againstThreshold, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, proposer); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertTrue(inVoting, "MaintenanceUpgrade should be in voting immediately"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } + + function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( + uint248 againstThreshold, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(inVoting, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitUpgradeProposal_TestFail +/// @notice Sad path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { + string proposalDescription; + + function setUp() public override { + super.setUp(); + + _setUpgradeProposalTypes(); + + proposalDescription = "Test upgrade proposal"; + } + + function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) + proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; // 50% + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { + uint248 zeroThreshold = 0; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { + // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR + excessiveThreshold = + uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(excessiveThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For MaintenanceUpgrade, mock the governor.proposeWithModule call + if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + } + + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + // Create new attestation for second attempt + bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); + + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_B); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, secondAttestation, proposalType); + } + + function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, proposalType), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); + } +} From 0f2aec9ef22b864f902bfd0bcfb516b7049926d3 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:10:33 -0300 Subject: [PATCH 15/73] fix: missing attestation validations (#434) * feat: add check for revoked attestations * feat: add valid attestation check in _validateTopDelegateAttestation function * refactor: use helper functions * fix: decoding after attestation existance validation * chore: remove redundant tests * fix: extra prank breaking tests * fix: pre-pr * refactor: improve variable naming --- .../governance/IProposalValidator.sol | 2 + .../snapshots/abi/ProposalValidator.json | 13 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 13 ++- .../test/governance/ProposalValidator.t.sol | 83 +++++++++++++++++-- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 815b321a5df..7ce083e8adb 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -155,6 +155,8 @@ interface IProposalValidator is ISemver { function distributionThreshold() external view returns (uint256); + function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); + function VOTING_TOKEN() external view returns (IGovernanceToken); function GOVERNOR() external view returns (IOptimismGovernor); diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9b982f52282..21b73e82298 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -51,6 +51,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "OPTIMISTIC_MODULE_PERCENT_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f5ed5805cf8..cd05d22a426 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x5c805e6dba872f7d2f16cb27a1ab7f8b31813e2b79b04d5ea61055bd06f0bf74", - "sourceCodeHash": "0xc1b29b287e1ff7df81aa3bc740fe63fc0203a976d222b1241636b639a478b791" + "initCodeHash": "0xc4efda2929244bf984fd5a3e32b6a8b5fb68622af6b05a31d3e5f7a25cd6bd3b", + "sourceCodeHash": "0x0064ec36b626190c1d2460aac284df6eaddcb51bf03ff60fa45aecad4ea922c6" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3f3146f00d6..c4d59230f1a 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -688,6 +688,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidAttestation(); } + // check if the attestation is revoked + if (attestation.revocationTime != 0) { + revert ProposalValidator_AttestationRevoked(); + } + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( @@ -712,7 +717,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bool canApprove_) { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); - (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + + // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) + if (attestation.uid == bytes32(0)) { + revert ProposalValidator_InvalidAttestation(); + } // check if the schema is correct if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { @@ -724,6 +733,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + // check if the attestation includes partial delegation or the recipient is not the caller if (_includePartialDelegation || attestation.recipient != _delegate) { revert ProposalValidator_InvalidAttestation(); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index ede38a009b1..fb0e277a3d3 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -466,7 +466,7 @@ contract ProposalValidator_Init is CommonTest { // Create schemas vm.prank(owner); APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), true ); vm.prank(owner); @@ -498,7 +498,7 @@ contract ProposalValidator_Init is CommonTest { data: AttestationRequestData({ recipient: address(0), expirationTime: 0, - revocable: false, + revocable: true, refUID: bytes32(0), data: abi.encode(_delegate, _proposalType), value: 0 @@ -724,6 +724,34 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); } + + function test_approveProposal_nonExistentAttestation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + bytes32 _nonExistentAttestationUid + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Ensure the attestation uid is not one of the valid ones + vm.assume( + _nonExistentAttestationUid != topDelegateAttestation_A + && _nonExistentAttestationUid != topDelegateAttestation_B + && _nonExistentAttestationUid != topDelegateAttestation_C + && _nonExistentAttestationUid != topDelegateAttestation_D + ); + + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + + // Expect the invalid attestation error to be reverted when attestation doesn't exist + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + } } // /// @title ProposalValidator_MoveToVote_Test @@ -848,9 +876,9 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { bool canApprove; // Expect the invalid attestation error to be reverted - vm.expectRevert(); - try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result) { - canApprove = result; + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result_) { + canApprove = result_; } catch { canApprove = false; } @@ -1682,6 +1710,27 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid ); } + + function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { + // Create valid attestation first (make it revocable) + bytes32 revocableAttestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid + ); + } } /// @title ProposalValidator_SubmitUpgradeProposal_Test @@ -2009,4 +2058,28 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.prank(topDelegate_A); validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); } + + function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + + // Create valid attestation first (make it revocable) + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: attestationUid, value: 0 }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } } From 162041544d3231b5db4a84718acab85c79f15e0c Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:58:07 -0300 Subject: [PATCH 16/73] test: add version function test (#438) --- .../test/governance/ProposalValidator.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fb0e277a3d3..2acaae32d85 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -887,6 +887,15 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { } } +/// @title ProposalValidator_Version_Test +/// @notice Tests for the version function +contract ProposalValidator_Version_Test is ProposalValidator_Init { + function test_version_succeeds() public { + string memory versionString = validator.version(); + assertEq(versionString, "1.0.0-beta.1"); + } +} + /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { From bc169ff840b55e25245be491e7f07ce3a982b56b Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:29:30 +0300 Subject: [PATCH 17/73] feat: move to vote (#435) * refactor: improve construction of approval voting module options * feat: move to vote logic * refactor: code blocks order * fix: proposal types data in initiallize test * feat: add move to vote tests * chore: rename inVoting * fix: semgrep * fix: pre-pr * fix: improve does not exist error * fix: improve error name * fix: test * fix: use const var instead of hardcoding test value * fix: add approved check and test * fix: improve tests --- .../governance/IProposalValidator.sol | 132 +- .../snapshots/abi/ProposalValidator.json | 118 +- .../src/governance/ProposalValidator.sol | 511 ++++++-- .../test/governance/ProposalValidator.t.sol | 1159 ++++++++++++++--- 4 files changed, 1529 insertions(+), 391 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7ce083e8adb..2ba27a28d83 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -10,43 +10,27 @@ import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. interface IProposalValidator is ISemver { + error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); + error ProposalValidator_ProposalAlreadyMovedToVote(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_VotingCycleAlreadySet(); error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_ProposalTypesDataLengthMismatch(); - error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); error ProposalValidator_AttestationRevoked(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); - error ProposalValidator_InvalidUpgradeProposalType(); error ProposalValidator_InvalidAgainstThreshold(); - - struct ProposalData { - address proposer; - ProposalType proposalType; - bool inVoting; - mapping(address => bool) delegateApprovals; - uint256 approvalCount; - } - - struct ProposalTypeData { - uint256 requiredApprovals; - uint8 proposalVotingModule; - } - - enum ProposalType { - ProtocolOrGovernorUpgrade, - MaintenanceUpgrade, - CouncilMemberElections, - GovernanceFund, - CouncilBudget - } + error ProposalValidator_InvalidUpgradeProposalType(); + error ProposalValidator_InvalidVotingCycle(); + error ProposalValidator_ProposalIdMismatch(); + error ProposalValidator_InvalidProposer(); + error ProposalValidator_InvalidProposal(); event ProposalSubmitted( bytes32 indexed proposalHash, @@ -89,34 +73,49 @@ interface IProposalValidator is ISemver { event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + struct ProposalData { + address proposer; + ProposalType proposalType; + bool movedToVote; + mapping(address => bool) delegateApprovals; + uint256 approvalCount; + uint256 votingCycle; + } - function moveToVote( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description - ) external returns (uint256 governorProposalId_); + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalVotingModule; + } - function setDistributionThreshold(uint256 _distributionThreshold) external; + struct VotingCycleData { + uint256 startingBlock; + uint256 duration; + uint256 votingCycleDistributionLimit; + uint256 movedToVoteTokenCount; + } - function setProposalTypeData( - ProposalType _proposalType, - ProposalTypeData memory _proposalTypeData - ) external; + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } - function setVotingCycleData( - uint256 _cycleNumber, - uint256 _startBlock, - uint256 _duration, - uint256 _votingCycleDistributionLimit - ) external; + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType, + uint256 _votingCycle + ) external returns (bytes32 proposalHash_); function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, string memory _proposalDescription, - bytes32 _attestationUid + bytes32 _attestationUid, + uint256 _votingCycle ) external returns (bytes32 proposalHash_); function submitFundingProposal( @@ -125,16 +124,48 @@ interface IProposalValidator is ISemver { address[] memory _optionsRecipients, uint256[] memory _optionsAmounts, string memory _description, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_); - function submitUpgradeProposal( + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + + function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, - string memory _proposalDescription, - bytes32 _attestationUid, + string memory _proposalDescription + ) external returns (bytes32 proposalHash_); + + function moveToVoteCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + string memory _proposalDescription + ) external returns (bytes32 proposalHash_); + + function moveToVoteFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, ProposalType _proposalType ) external returns (bytes32 proposalHash_); + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) external; + + function setDistributionThreshold(uint256 _distributionThreshold) external; + + function setProposalTypeData( + ProposalType _proposalType, + ProposalTypeData memory _proposalTypeData + ) external; + function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, @@ -151,12 +182,8 @@ interface IProposalValidator is ISemver { function transferOwnership(address newOwner) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); - function distributionThreshold() external view returns (uint256); - function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function VOTING_TOKEN() external view returns (IGovernanceToken); function GOVERNOR() external view returns (IOptimismGovernor); @@ -169,6 +196,8 @@ interface IProposalValidator is ISemver { function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); + function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); @@ -176,7 +205,8 @@ interface IProposalValidator is ISemver { function votingCycles(uint256) external view returns ( uint256 startingBlock, uint256 duration, - uint256 votingCycleDistributionLimit + uint256 votingCycleDistributionLimit, + uint256 movedToVoteTokenCount ); function __constructor__( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 21b73e82298..4af4cf80adf 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -225,33 +225,96 @@ }, { "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + } + ], + "name": "moveToVoteCouncilMemberElectionsProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, { "internalType": "address[]", - "name": "_targets", + "name": "_optionsRecipients", "type": "address[]" }, { "internalType": "uint256[]", - "name": "_values", + "name": "_optionsAmounts", "type": "uint256[]" }, { - "internalType": "bytes[]", - "name": "_calldatas", - "type": "bytes[]" + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "moveToVoteFundingProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint248", + "name": "_againstThreshold", + "type": "uint248" }, { "internalType": "string", - "name": "_description", + "name": "_proposalDescription", "type": "string" } ], - "name": "moveToVote", + "name": "moveToVoteProtocolOrGovernorUpgradeProposal", "outputs": [ { - "internalType": "uint256", - "name": "governorProposalId_", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" } ], "stateMutability": "nonpayable", @@ -406,6 +469,11 @@ "internalType": "bytes32", "name": "_attestationUid", "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitCouncilMemberElectionsProposal", @@ -450,6 +518,11 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "_proposalType", "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitFundingProposal", @@ -484,6 +557,11 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "_proposalType", "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitUpgradeProposal", @@ -547,6 +625,11 @@ "internalType": "uint256", "name": "votingCycleDistributionLimit", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "movedToVoteTokenCount", + "type": "uint256" } ], "stateMutability": "view", @@ -791,11 +874,21 @@ "name": "ProposalValidator_InvalidUpgradeProposalType", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidVotingCycle", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyMovedToVote", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadySubmitted", @@ -806,6 +899,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalIdMismatch", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalTypesDataLengthMismatch", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c4d59230f1a..bd8167e57a0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -43,6 +43,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when attempting to move a proposal to vote that is already in voting. error ProposalValidator_ProposalAlreadySubmitted(); + /// @notice Thrown when attempting to move a proposal to vote that is already in voting. + error ProposalValidator_ProposalAlreadyMovedToVote(); + /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); @@ -79,6 +82,65 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an invalid proposal type is provided for upgrade proposals. error ProposalValidator_InvalidUpgradeProposalType(); + /// @notice Thrown when the trying to move a proposal to vote outside of the accepted voting cycle. + error ProposalValidator_InvalidVotingCycle(); + + /// @notice Thrown when the proposalId returned by the Governor is not the same as the proposalHash. + error ProposalValidator_ProposalIdMismatch(); + + /// @notice Thrown when the caller is not the proposer. + error ProposalValidator_InvalidProposer(); + + /// @notice Thrown when the proposal is invalid trying to move to vote. + error ProposalValidator_InvalidProposal(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new proposal is submitted. + /// @param proposalHash The hash of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + ); + + /// @notice Emitted when a delegate approves a proposal. + /// @param proposalHash The hash of the approved proposal. + /// @param approver The address of the delegate who approved the proposal. + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + + /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. + /// @param proposalHash The hash of the proposal moved to vote. + /// @param executor The address that executed the move to vote. + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + + /// @notice Emitted when the voting cycle data is set. + /// @param cycleNumber The number of the voting cycle. + /// @param startBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + + /// @notice Emitted when the distribution threshold is set. + /// @param newDistributionThreshold The new distribution threshold. + event DistributionThresholdSet(uint256 newDistributionThreshold); + + /// @notice Emitted when the proposal type data is set. + /// @param proposalType The type of proposal. + /// @param requiredApprovals The required number of approvals. + /// @param proposalVotingModule The proposal type ID. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + + /// @notice Emitted with ProposalSubmitted event. + /// @param proposalHash The hash of the submitted proposal. + /// @param encodedVotingModuleData The encoded voting module data. + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -86,15 +148,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing proposal information. /// @param proposer The address that submitted the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. - /// @param inVoting Whether the proposal has been moved to the voting phase. + /// @param movedToVote Whether the proposal has been proposed to the Governor for voting. /// @param delegateApprovals Mapping of delegate addresses to their approval status. /// @param approvalCount Number of approvals received so far. + /// @param votingCycle The voting cycle number the proposal is targetted for. struct ProposalData { address proposer; ProposalType proposalType; - bool inVoting; + bool movedToVote; mapping(address => bool) delegateApprovals; uint256 approvalCount; + uint256 votingCycle; } /// @notice Struct for storing explicit data for each proposal type. @@ -110,10 +174,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param startingBlock The block number of the starting block of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. + /// @param movedToVoteTokenCount The total amount of tokens to possibly be distributed in the voting cycle. struct VotingCycleData { uint256 startingBlock; uint256 duration; uint256 votingCycleDistributionLimit; + uint256 movedToVoteTokenCount; } /*////////////////////////////////////////////////////////////// @@ -143,52 +209,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; /*////////////////////////////////////////////////////////////// - EVENTS + STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new proposal is submitted. - /// @param proposalHash The hash of the submitted proposal. - /// @param proposer The address that submitted the proposal. - /// @param description Description of the proposal. - /// @param proposalType Type of the proposal. - event ProposalSubmitted( - bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType - ); - - /// @notice Emitted when a delegate approves a proposal. - /// @param proposalHash The hash of the approved proposal. - /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); - - /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalHash The hash of the proposal moved to vote. - /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); - - /// @notice Emitted when the voting cycle data is set. - /// @param cycleNumber The number of the voting cycle. - /// @param startBlock The block number of the starting block of the voting cycle. - /// @param duration The duration of the voting cycle. - /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. - event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit - ); - - /// @notice Emitted when the distribution threshold is set. - /// @param newDistributionThreshold The new distribution threshold. - event DistributionThresholdSet(uint256 newDistributionThreshold); - - /// @notice Emitted when the proposal type data is set. - /// @param proposalType The type of proposal. - /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal type ID. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); - - /// @notice Emitted with ProposalSubmitted event. - /// @param proposalHash The hash of the submitted proposal. - /// @param encodedVotingModuleData The encoded voting module data. - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -292,12 +315,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, bytes32 _attestationUid, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -348,13 +373,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) if (_proposalType == ProposalType.MaintenanceUpgrade) { - proposal.inVoting = true; + proposal.movedToVote = true; GOVERNOR.proposeWithModule( VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) @@ -370,12 +396,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionDescriptions The strings of the different options that can be voted. /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, string memory _proposalDescription, - bytes32 _attestationUid + bytes32 _attestationUid, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -394,22 +422,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidCriteriaValue(); } - ProposalOption[] memory options = new ProposalOption[](optionsLength); - - // Build proposal options without any execution calls (elections don't execute operations) - for (uint256 i = 0; i < optionsLength; i++) { - address[] memory targets = new address[](0); - uint256[] memory values = new uint256[](0); - bytes[] memory calldatas = new bytes[](0); - - options[i] = ProposalOption({ - budgetTokensSpent: 0, // No tokens spent for elections - targets: targets, - values: values, - calldatas: calldatas, - description: _optionDescriptions[i] - }); - } + // Build proposal options (elections don't execute operations) + (ProposalOption[] memory options,) = + _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria ApprovalProposalSettings memory settings = ApprovalProposalSettings({ @@ -446,6 +461,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = ProposalType.CouncilMemberElections; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -463,6 +479,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitFundingProposal( uint128 _criteriaValue, @@ -470,7 +487,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address[] memory _optionsRecipients, uint256[] memory _optionsAmounts, string memory _description, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -491,32 +509,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidOptionsLength(); } - ProposalOption[] memory options = new ProposalOption[](optionsLength); - uint256 totalBudget = 0; - - // Check amounts, build options, and calculate total budget in single loop - for (uint256 i = 0; i < optionsLength; i++) { - if (_optionsAmounts[i] > distributionThreshold) { - revert ProposalValidator_ExceedsDistributionThreshold(); - } - - address[] memory targets = new address[](1); - uint256[] memory values = new uint256[](1); - bytes[] memory calldatas = new bytes[](1); - - targets[0] = Predeploys.GOVERNANCE_TOKEN; - calldatas[0] = abi.encodeCall(IERC20.transfer, (_optionsRecipients[i], _optionsAmounts[i])); - - options[i] = ProposalOption({ - budgetTokensSpent: _optionsAmounts[i], - targets: targets, - values: values, - calldatas: calldatas, - description: _optionsDescriptions[i] - }); - - totalBudget += _optionsAmounts[i]; - } + // Build proposal options with funding execution data + (ProposalOption[] memory options, uint256 totalBudget) = + _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings ApprovalProposalSettings memory settings = ApprovalProposalSettings({ @@ -551,6 +546,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -583,53 +579,263 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalApproved(_proposalHash, _delegate); } - /// @notice Move a proposal to voting phase after sufficient delegate approvals - /// @param _targets Target addresses for proposal calls - /// @param _values ETH values for proposal calls - /// @param _calldatas Function data for proposal calls - /// @param _description Description of the proposal - /// @return governorProposalId_ The proposal ID in the governor contract - function moveToVote( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + /// @notice Checks if a delegate can approve a proposal. + /// @dev Helper function for UI integration. + /// @param _attestationUid The UID of the attestation to check. + /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + } + + /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. + /// @param _againstThreshold The threshold for the proposal to be against the total supply. + /// @param _proposalDescription Description of the proposal. + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteProtocolOrGovernorUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription ) external - returns (uint256 governorProposalId_) + returns (bytes32 proposalHash_) { - // Verify that the provided data matches the proposalHash - bytes32 _proposalHash = bytes32(0); // TODO: Implement hashProposalWithModule + // Configure optimistic proposal settings + OptimisticProposalSettings memory settings = + OptimisticProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); - ProposalData storage proposal = _proposals[_proposalHash]; + bytes memory proposalVotingModuleData = abi.encode(settings); - if (proposal.proposer == address(0)) { - revert ProposalValidator_ProposalDoesNotExist(); + // Get the module address from the configurator + ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; + address votingModule = proposalTypesConfigurator.proposalTypes( + proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule + ).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist and be valid + if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if the caller is the proposer + if (proposal.proposer != _msgSender()) { + revert ProposalValidator_InvalidProposer(); } - ProposalTypeData memory proposalTypeData = proposalTypesData[proposal.proposalType]; - if (proposal.approvalCount < proposalTypeData.requiredApprovals) { + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } - if (proposal.inVoting) { - revert ProposalValidator_ProposalAlreadySubmitted(); + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); } - proposal.inVoting = true; + proposal.movedToVote = true; - governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, uint8(proposal.proposalType)); + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), + proposalVotingModuleData, + _proposalDescription, + uint8(proposalType) + ); - emit ProposalMovedToVote(_proposalHash, msg.sender); + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); } - /// @notice Checks if a delegate can approve a proposal. - /// @dev Helper function for UI integration. - /// @param _attestationUid The UID of the attestation to check. - /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + /// @notice Moves a council member elections proposal to vote by proposing it on the Governor. + /// @param _criteriaValue The number of top choices that can pass the voting. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _proposalDescription Description of the proposal. + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + string memory _proposalDescription + ) + external + returns (bytes32 proposalHash_) + { + // Configure approval module options + (ProposalOption[] memory options,) = + _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); + + // Configure approval module settings + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + maxApprovals: uint8(_optionsDescriptions.length), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), + criteriaValue: _criteriaValue, + budgetAmount: 0 + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + ProposalType proposalType = ProposalType.CouncilMemberElections; + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist and be valid + if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if the caller is the proposer + if (proposal.proposer != _msgSender()) { + revert ProposalValidator_InvalidProposer(); + } + + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { + revert ProposalValidator_InsufficientApprovals(); + } + + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + + // Check if the voting cycle is valid + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + // TODO: is + duration correct? + if ( + votingCycleData.startingBlock > block.number + || votingCycleData.startingBlock + votingCycleData.duration < block.number + ) { + revert ProposalValidator_InvalidVotingCycle(); + } + + proposal.movedToVote = true; + + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + ); + + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); + } + + /// @notice Moves a funding proposal to vote by proposing it on the Governor. + /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), + /// then convert to the absolute vote count by calculating: (percentage / 100) * total_votable_supply. + /// Direct contract callers must provide the absolute number of votes required for passage. + /// @param _criteriaValue The absolute number of votes required for the proposal to pass. This represents the + /// threshold that must be met or exceeded for any option to be considered successful. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _optionsRecipients An address for each option to transfer funds to in case the option passes the voting. + /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. + /// @param _description Description of the proposal. + /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + uint256 optionsLength = _optionsDescriptions.length; + // Only funding proposal types can use this function + if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { + revert ProposalValidator_InvalidFundingProposalType(); + } + + // Configure approval module options + (ProposalOption[] memory options, uint256 totalBudget) = + _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); + + // Configure approval module settings + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: _criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist + if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[_proposalType].requiredApprovals) { + revert ProposalValidator_InsufficientApprovals(); + } + + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + + // Check if proposal can be moved to vote + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + // TODO: is + duration correct? + if ( + votingCycleData.startingBlock > block.number + || votingCycleData.startingBlock + votingCycleData.duration < block.number + ) { + revert ProposalValidator_InvalidVotingCycle(); + } + + // Check if total budget is within the voting cycle distribution limit + if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + + // Move proposal to vote + proposal.movedToVote = true; + votingCycles[proposal.votingCycle].movedToVoteTokenCount += totalBudget; + + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _description, uint8(_proposalType) + ); + + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); } /// @notice Sets the data of a voting cycle. @@ -743,6 +949,62 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { canApprove_ = true; } + /// @notice Internal function to build proposal options with optional execution data. + /// @param _optionDescriptions The strings of the different options that can be voted. + /// @param _recipients An address for each option to transfer funds to (empty for non-funding proposals). + /// @param _amounts The amount to transfer for each option (empty for non-funding proposals). + /// @return options_ The built proposal options. + /// @return totalBudget_ The total budget amount (sum of all amounts, 0 for non-funding proposals). + function _buildApprovalModuleOptions( + string[] memory _optionDescriptions, + address[] memory _recipients, + uint256[] memory _amounts + ) + internal + view + returns (ProposalOption[] memory options_, uint256 totalBudget_) + { + uint256 optionsLength = _optionDescriptions.length; + options_ = new ProposalOption[](optionsLength); + + for (uint256 i = 0; i < optionsLength; i++) { + address[] memory targets; + uint256[] memory values; + bytes[] memory calldatas; + uint256 budgetTokensSpent; + + // Check if this is a funding proposal (has recipients and amounts) + if (_recipients.length > 0 && _amounts.length > 0) { + // Validate amount doesn't exceed distribution threshold + if (_amounts[i] > distributionThreshold) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + targets = new address[](1); + values = new uint256[](1); + calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (_recipients[i], _amounts[i])); + budgetTokensSpent = _amounts[i]; + totalBudget_ += _amounts[i]; + } else { + // Non-funding proposals have no execution data + targets = new address[](0); + values = new uint256[](0); + calldatas = new bytes[](0); + budgetTokensSpent = 0; + } + + options_[i] = ProposalOption({ + budgetTokensSpent: budgetTokensSpent, + targets: targets, + values: values, + calldatas: calldatas, + description: _optionDescriptions[i] + }); + } + } + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. @@ -780,7 +1042,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingCycles[_cycleNumber] = VotingCycleData({ startingBlock: _startBlock, duration: _duration, - votingCycleDistributionLimit: _votingCycleDistributionLimit + votingCycleDistributionLimit: _votingCycleDistributionLimit, + movedToVoteTokenCount: 0 }); emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2acaae32d85..7721ac54f9e 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -73,25 +73,35 @@ contract ProposalValidatorForTest is ProposalValidator { function getProposalData(bytes32 _proposalHash) public view - returns (address proposer_, ProposalType proposalType_, bool inVoting_, uint256 approvalCount_) + returns ( + address proposer_, + ProposalType proposalType_, + bool movedToVote_, + uint256 approvalCount_, + uint256 votingCycle_ + ) { ProposalData storage proposal = _proposals[_proposalHash]; - return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); + return ( + proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle + ); } function setProposalData( bytes32 _proposalHash, address _proposer, ProposalType _proposalType, - bool _inVoting, - uint256 _approvalCount + bool _movedToVote, + uint256 _approvalCount, + uint256 _votingCycle ) public { _proposals[_proposalHash].proposer = _proposer; _proposals[_proposalHash].proposalType = _proposalType; - _proposals[_proposalHash].inVoting = _inVoting; + _proposals[_proposalHash].movedToVote = _movedToVote; _proposals[_proposalHash].approvalCount = _approvalCount; + _proposals[_proposalHash].votingCycle = _votingCycle; } function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { @@ -112,9 +122,9 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; uint256 public constant DURATION = 100; - uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; + uint256 public constant DISTRIBUTION_LIMIT = 20000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; - uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; + uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 1; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; @@ -130,6 +140,7 @@ contract ProposalValidator_Init is CommonTest { bytes32 topDelegateAttestation_B; bytes32 topDelegateAttestation_C; bytes32 topDelegateAttestation_D; + address approvedProposer = makeAddr("approvedProposer"); address approvalVotingModule; address optimisticVotingModule; @@ -277,22 +288,27 @@ contract ProposalValidator_Init is CommonTest { proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); + // ProtocolOrGovernorUpgrade proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID }); + // MaintenanceUpgrade proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 + requiredApprovals: 0, + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID }); + // CouncilMemberElections proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 2 + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); + // GovernanceFund proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); + // CouncilBudget proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, proposalVotingModule: APPROVAL_VOTING_MODULE_ID @@ -301,7 +317,7 @@ contract ProposalValidator_Init is CommonTest { return (proposalTypes, proposalTypesData); } - function _constructVotingModuleData( + function _constructFundingVotingModuleData( string[] memory descriptions, address[] memory recipients, uint256[] memory amounts, @@ -395,6 +411,65 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(settings); } + /// @notice Helper function to create a proposal for move to vote + function _createUpgradeProposalForMoveToVote( + address proposer, + uint248 againstThreshold, + string memory proposalDescription + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + // Calculate expected proposal hash + votingModuleData_ = _constructOptimisticVotingModuleData(againstThreshold); + proposalHash_ = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + // 1 vote as default for being able to move to vote + validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + + /// @notice Helper function to create a proposal for move to vote for council elections + function _createCouncilElectionProposalForMoveToVote( + address proposer, + uint128 criteriaValue, + string[] memory optionsDescriptions, + string memory proposalDescription + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + votingModuleData_ = _constructCouncilElectionVotingModuleData(optionsDescriptions, criteriaValue); + proposalHash_ = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + + /// @notice Helper function to create a proposal for move to vote for a funding proposal type + function _createFundingProposalForMoveToVote( + address proposer, + uint128 criteriaValue, + string[] memory optionsDescriptions, + address[] memory optionsRecipients, + uint256[] memory optionsAmounts, + string memory proposalDescription, + ProposalValidator.ProposalType proposalType + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + votingModuleData_ = + _constructFundingVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + proposalHash_ = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + validator.setProposalData(proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + /// @notice Helper function to setup proposal types configurator mocks function _mockProposalTypesConfiguratorCall(uint8 _votingModuleId) internal { address moduleAddress; @@ -524,26 +599,6 @@ contract ProposalValidator_Init is CommonTest { }) ); } - - /// @notice Helper to create a standard proposal setup - function _createProposalSetup() - internal - view - returns ( - address[] memory targets_, - uint256[] memory values_, - bytes[] memory calldatas_, - string memory description_ - ) - { - targets_ = new address[](1); - targets_[0] = address(0); - values_ = new uint256[](1); - values_[0] = 0; - calldatas_ = new bytes[](1); - calldatas_[0] = bytes(""); - description_ = "Test proposal"; - } } /// @title ProposalValidator_ApproveProposal_Test @@ -558,7 +613,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect event to be emitted when approving vm.expectEmit(address(validator)); @@ -571,7 +626,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { // Check that the proposal data has been updated assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - (,,, uint256 approvalCount) = validator.getProposalData(_proposalHash); + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); assertEq(approvalCount, 1); } } @@ -600,7 +655,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal as already approved by the top delegate validator.mockApproveProposal(_proposalHash, topDelegate_A); @@ -637,7 +692,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); @@ -649,7 +704,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // revoke the attestation vm.prank(owner); @@ -682,7 +737,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -701,7 +756,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // create an attestation with partial delegation vm.prank(owner); @@ -745,7 +800,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -754,130 +809,760 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { } } -// /// @title ProposalValidator_MoveToVote_Test -// /// @notice Happy path tests for moveToVote function -// contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { -// address[] targets; -// uint256[] values; -// bytes[] calldatas; -// string description; -// ProposalValidator.ProposalType proposalType; -// uint8 proposalVotingModule; - -// function setUp() public override { -// super.setUp(); - -// (targets, values, calldatas, description) = _createProposalSetup(); - -// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; -// proposalVotingModule = 0; -// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - -// /* vm.prank(topDelegate_A); */ -// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); -// _approveProposal(topDelegate_D, proposalHash); -// } - -// function test_moveToVote_succeeds() public { -// _mockAndExpect( -// address(governor), -// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, -// proposalVotingModule)), -// abi.encode(1) -// ); - -// // Expect the ProposalMovedToVote event to be emitted -// vm.expectEmit(address(validator)); -// emit ProposalMovedToVote(proposalHash, owner); - -// vm.prank(owner); -// uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); - -// assertEq(governorProposalId, 1); -// } -// } - -// /// @title ProposalValidator_MoveToVote_TestFail -// /// @notice Sad path tests for moveToVote function -// contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { -// address[] targets; -// uint256[] values; -// bytes[] calldatas; -// string description; -// ProposalValidator.ProposalType proposalType; -// uint8 proposalVotingModule; - -// function setUp() public override { -// super.setUp(); - -// (targets, values, calldatas, description) = _createProposalSetup(); - -// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; -// proposalVotingModule = 0; -// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - -// /* vm.prank(topDelegate_A); */ -// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented -// } - -// function test_moveToVote_insufficientApprovals_reverts() public { -// // Only approve with 3 delegates (need 4) -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); - -// vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); -// } - -// function test_moveToVote_alreadyProposed_reverts() public { -// // Approve with all 4 delegates -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); -// _approveProposal(topDelegate_D, proposalHash); - -// _mockAndExpect( -// address(governor), -// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, -// proposalVotingModule)), -// abi.encode(1) -// ); - -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); - -// vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); -// } -// } +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( + approvedProposer, againstThreshold, proposalDescription + ); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( + approvedProposer, againstThreshold, proposalDescription + ); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) + public + { + vm.assume(_caller != approvedProposer); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts( + uint248 _againstThreshold + ) + public + { + // This will generate a different proposal hash which will make the proposal type wrong + vm.assume(_againstThreshold != againstThreshold); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } +} + +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + bytes32 expectedHash; + bytes votingModuleData; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + proposalDescription + ); + } + + function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + bytes32 expectedHash; + bytes votingModuleData; + + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + proposalDescription + ); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) + public + { + vm.assume(_caller != approvedProposer); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { + // This will generate a different proposal hash which will make the proposal type wrong + uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(block.number + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } +} + +contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions = new string[](2); + address[] optionsRecipients = new address[](2); + uint256[] optionsAmounts = new uint256[](2); + bytes32 expectedGovernanceFundHash; + bytes32 expectedCouncilBudgetHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; + + function setUp() public override { + super.setUp(); + + // Create option descriptions for the proposals + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + + // Create option recipients for the proposals + optionsRecipients[0] = makeAddr("optionRecipient1"); + optionsRecipients[1] = makeAddr("optionRecipient2"); + + // Create option amounts for the proposals + optionsAmounts[0] = 100 ether; + optionsAmounts[1] = 200 ether; + + // Create one proposal for each type + (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } + + function test_moveToVoteFundingProposal_governanceFund_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + governanceFundVotingModuleData, + governanceFundProposalDescription, + uint8(governanceFundProposalType) + ) + ), + abi.encode(uint256(expectedGovernanceFundHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } + + function test_moveToVoteFundingProposal_councilBudget_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + councilBudgetVotingModuleData, + councilBudgetProposalDescription, + uint8(councilBudgetProposalType) + ) + ), + abi.encode(uint256(expectedCouncilBudgetHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } +} + +contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + bytes32 governanceFundExpectedHash; + bytes32 councilBudgetExpectedHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; + + function setUp() public override { + super.setUp(); + + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } + + function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( + uint8 _proposalTypeValue, + string memory _proposalDescription + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposal_reverts( + uint8 _proposalTypeValue, + uint128 _criteriaValue + ) + public + { + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // not find the proposal + vm.assume(_criteriaValue != criteriaValue); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( + uint8 _wrongProposalTypeValue, + uint8 _validProposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); + + _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); + + string memory proposalDescription; + if (validProposalType == governanceFundProposalType) { + // Set proposal data proposal type to a different value + validator.setProposalData(governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data proposal type to a different value + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + proposalDescription, + validProposalType + ); + } + + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data movedToVote to true + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data movedToVote to true + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(START_BLOCK + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + uint8 _proposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Set the first option amount to exceed the distribution threshold + optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + string[] memory _optionsDescriptions = new string[](3); + address[] memory _optionsRecipients = new address[](3); + uint256[] memory _optionsAmounts = new uint256[](3); + + _optionsDescriptions[0] = "Option 1"; + _optionsDescriptions[1] = "Option 2"; + _optionsDescriptions[2] = "Option 3"; + + _optionsRecipients[0] = makeAddr("optionRecipient1"); + _optionsRecipients[1] = makeAddr("optionRecipient2"); + _optionsRecipients[2] = makeAddr("optionRecipient3"); + + _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; + + _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + _optionsDescriptions, + _optionsRecipients, + _optionsAmounts, + proposalDescription, + proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + vm.roll(START_BLOCK + 1); + validator.moveToVoteFundingProposal( + criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( + uint8 _proposalTypeValue, + bytes32 _randomHash + ) + public + { + vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + bytes memory votingModuleData; + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + votingModuleData = governanceFundVotingModuleData; + proposalDescription = governanceFundProposalDescription; + } else { + votingModuleData = councilBudgetVotingModuleData; + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } +} /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnsTrue_succeeds() public { + function test_canApproveProposal_returnTrue_succeeds() public { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); } - function test_canApproveProposal_returnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { // Ensure the attestation uid is not one of the top delegates vm.assume( - _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B - && _attestationUid != topDelegateAttestation_C && _attestationUid != topDelegateAttestation_D + attestationUid != topDelegateAttestation_A && attestationUid != topDelegateAttestation_B + && attestationUid != topDelegateAttestation_C && attestationUid != topDelegateAttestation_D ); bool canApprove; // Expect the invalid attestation error to be reverted - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); - try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result_) { + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { canApprove = result_; } catch { canApprove = false; @@ -916,12 +1601,17 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - (uint256 actualStartBlock, uint256 actualDuration, uint256 actualDistributionLimit) = - validator.votingCycles(cycleNumber); + ( + uint256 actualStartBlock, + uint256 actualDuration, + uint256 actualDistributionLimit, + uint256 actualMovedToVoteTokenCount + ) = validator.votingCycles(cycleNumber); assertEq(actualStartBlock, startBlock); assertEq(actualDuration, duration); assertEq(actualDistributionLimit, distributionLimit); + assertEq(actualMovedToVoteTokenCount, 0); } function test_setVotingCycleData_notOwner_reverts() public { @@ -1079,7 +1769,8 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init } // Calculate expected proposal hash - bytes memory votingModuleData = _constructVotingModuleData(descriptions, recipients, amounts, criteriaValue); + bytes memory votingModuleData = + _constructFundingVotingModuleData(descriptions, recipients, amounts, criteriaValue); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1101,8 +1792,9 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(proposer); - bytes32 proposalHash = - validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); + bytes32 proposalHash = validator.submitFundingProposal( + criteriaValue, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1110,14 +1802,16 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(inVoting, "Proposal should not be in voting yet"); + assertFalse(movedToVote, "Proposal should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1144,7 +1838,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1177,7 +1871,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingRecipients, matchingAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1210,7 +1905,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedRecipients, matchingAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1243,7 +1939,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingRecipients, mismatchedAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1268,7 +1965,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1282,7 +1979,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); + _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1298,7 +1995,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); // Attempt to submit identical proposal @@ -1308,7 +2005,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1322,7 +2019,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); + _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1339,7 +2036,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1354,7 +2051,13 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, emptyDescriptions, emptyRecipients, emptyAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, + emptyDescriptions, + emptyRecipients, + emptyAmounts, + description, + proposalType, + CYCLE_NUMBER ); } @@ -1376,7 +2079,13 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, tooManyDescriptions, tooManyRecipients, tooManyAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, + tooManyDescriptions, + tooManyRecipients, + tooManyAmounts, + description, + proposalType, + CYCLE_NUMBER ); } } @@ -1425,24 +2134,32 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { assertEq(validator.owner(), owner); // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit) = validator.votingCycles(CYCLE_NUMBER); + (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + validator.votingCycles(CYCLE_NUMBER); assertEq(startBlock, START_BLOCK); assertEq(duration, DURATION); assertEq(distributionLimit, DISTRIBUTION_LIMIT); + assertEq(movedToVoteTokenCount, 0); // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); - assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { + assertEq(requiredApprovals, 0); + } else { + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + } - // Both GovernanceFund and CouncilBudget use APPROVAL_VOTING_MODULE_ID + // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID if ( proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections ) { assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); } else { - assertEq(proposalVotingModule, uint8(i)); + // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID + assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); } } } @@ -1541,14 +2258,19 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); assertEq(proposalHash, expectedHash); // Verify proposal data was stored correctly - (address proposer, ProposalValidator.ProposalType proposalType, bool inVoting, uint256 approvalCount) = - validator.getProposalData(proposalHash); + ( + address proposer, + ProposalValidator.ProposalType proposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); assertEq( @@ -1556,8 +2278,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal uint8(ProposalValidator.ProposalType.CouncilMemberElections), "Proposal type should be CouncilMemberElections" ); - assertFalse(inVoting, "Proposal should not be in voting yet"); + assertFalse(movedToVote, "Proposal should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1593,7 +2316,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER ); } @@ -1604,7 +2327,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1613,7 +2336,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal(criteriaValue, emptyOptions, proposalDescription, attestationUid); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); } function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { @@ -1635,7 +2360,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Submit first proposal vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); // Create new attestation for second attempt @@ -1649,7 +2374,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_B); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, secondAttestation + criteriaValue, optionDescriptions, proposalDescription, secondAttestation, CYCLE_NUMBER ); } @@ -1673,7 +2398,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1701,7 +2426,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, invalidAttestation + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER ); } @@ -1716,7 +2441,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1737,7 +2462,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER ); } } @@ -1808,8 +2533,9 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init emit ProposalMovedToVote(expectedHash, proposer); vm.prank(proposer); - bytes32 proposalHash = - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1817,14 +2543,16 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertTrue(inVoting, "MaintenanceUpgrade should be in voting immediately"); + assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( @@ -1867,8 +2595,9 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init emit ProposalVotingModuleData(expectedHash, votingModuleData); vm.prank(proposer); - bytes32 proposalHash = - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1876,14 +2605,16 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(inVoting, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1910,7 +2641,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { @@ -1922,7 +2655,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { @@ -1935,7 +2670,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I // Try to submit with different address than attested vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { @@ -1945,7 +2682,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { @@ -1958,7 +2695,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(excessiveThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { @@ -1998,7 +2737,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); // Create new attestation for second attempt bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); @@ -2009,7 +2750,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.prank(topDelegate_B); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, secondAttestation, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, secondAttestation, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { @@ -2038,7 +2781,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { @@ -2065,7 +2810,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { @@ -2089,6 +2836,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } } From 4613b94ca307e19ee59d1863e44974e015e12c5c Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:01:57 -0300 Subject: [PATCH 18/73] refactor: improve tests (#440) * refactor: order tests based on implementation * chore: remove unused variables * test: add fuzzing for setter tests * test: add fuzzing to hashProposalWithModule tests * test: improve funding proposal tests * test: improve council member election tests * fix: pre-pr --- .../src/governance/ProposalValidator.sol | 5 +- .../test/governance/ProposalValidator.t.sol | 3060 ++++++++--------- 2 files changed, 1522 insertions(+), 1543 deletions(-) diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index bd8167e57a0..a9559e32ba6 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -640,10 +640,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), - proposalVotingModuleData, - _proposalDescription, - uint8(proposalType) + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7721ac54f9e..fd369cf994c 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -133,13 +133,7 @@ contract ProposalValidator_Init is CommonTest { address owner; address user; address topDelegate_A = makeAddr("topDelegate_A"); - address topDelegate_B = makeAddr("topDelegate_B"); - address topDelegate_C = makeAddr("topDelegate_C"); - address topDelegate_D = makeAddr("topDelegate_D"); bytes32 topDelegateAttestation_A; - bytes32 topDelegateAttestation_B; - bytes32 topDelegateAttestation_C; - bytes32 topDelegateAttestation_D; address approvedProposer = makeAddr("approvedProposer"); address approvalVotingModule; address optimisticVotingModule; @@ -150,7 +144,6 @@ contract ProposalValidator_Init is CommonTest { IProposalTypesConfigurator public proposalTypesConfigurator; bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; - bytes32 public proposalHash; event ProposalSubmitted( bytes32 indexed proposalHash, @@ -225,12 +218,6 @@ contract ProposalValidator_Init is CommonTest { ); } - /// @notice Helper function to set both funding proposal types. - function _setFundingProposalTypes() internal { - _setGovernanceFundProposalType(); - _setCouncilBudgetProposalType(); - } - /// @notice Helper function to set ProtocolOrGovernorUpgrade proposal type data. function _setProtocolOrGovernorUpgradeProposalType() internal { _setProposalTypeData( @@ -253,11 +240,6 @@ contract ProposalValidator_Init is CommonTest { ); } - /// @notice Helper function to set both upgrade proposal types. - function _setUpgradeProposalTypes() internal { - _setProtocolOrGovernorUpgradeProposalType(); - _setMaintenanceUpgradeProposalType(); - } /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() @@ -427,7 +409,14 @@ contract ProposalValidator_Init is CommonTest { ); // 1 vote as default for being able to move to vote - validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, + proposer, + ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, + false, + PROPOSAL_REQUIRED_APPROVALS, + CYCLE_NUMBER + ); } /// @notice Helper function to create a proposal for move to vote for council elections @@ -445,7 +434,14 @@ contract ProposalValidator_Init is CommonTest { approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, + proposer, + ProposalValidator.ProposalType.CouncilMemberElections, + false, + PROPOSAL_REQUIRED_APPROVALS, + CYCLE_NUMBER + ); } /// @notice Helper function to create a proposal for move to vote for a funding proposal type @@ -467,7 +463,9 @@ contract ProposalValidator_Init is CommonTest { approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData(proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER + ); } /// @notice Helper function to setup proposal types configurator mocks @@ -553,9 +551,6 @@ contract ProposalValidator_Init is CommonTest { // Create attestations for top delegates topDelegateAttestation_A = _createTopDelegateAttestation(topDelegate_A); - topDelegateAttestation_B = _createTopDelegateAttestation(topDelegate_B); - topDelegateAttestation_C = _createTopDelegateAttestation(topDelegate_C); - topDelegateAttestation_D = _createTopDelegateAttestation(topDelegate_D); } /// @notice Helper to create a valid attestation for an approved proposer @@ -601,1171 +596,802 @@ contract ProposalValidator_Init is CommonTest { } } -/// @title ProposalValidator_ApproveProposal_Test -/// @notice Happy path tests for approveProposal function -contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Ensure the proposal hash is not 0 - vm.assume(_proposalHash != bytes32(0)); +/// @title ProposalValidator_Version_Test +/// @notice Tests for the version function +contract ProposalValidator_Version_Test is ProposalValidator_Init { + function test_version_succeeds() public { + string memory versionString = validator.version(); + assertEq(versionString, "1.0.0-beta.1"); + } +} - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_Initialize_Test +/// @notice Tests for the initialize function +contract ProposalValidator_Initialize_Test is ProposalValidator_Init { + /// @dev Override to create validator proxy without initialization for testing + function _initializeValidator() internal override { + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); + validator = ProposalValidatorForTest(address(new Proxy(owner))); + } - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(_proposalHash, topDelegate_A); + function test_initialize_succeeds() public { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); - // Approve the proposal, use the attestation of the top delegate that was created in setUp - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + proposalTypesConfigurator, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); - // Check that the proposal data has been updated - assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); + // Verify initialization was successful + assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.owner(), owner); - (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); - assertEq(approvalCount, 1); + // Verify voting cycle data + (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + validator.votingCycles(CYCLE_NUMBER); + assertEq(startBlock, START_BLOCK); + assertEq(duration, DURATION); + assertEq(distributionLimit, DISTRIBUTION_LIMIT); + assertEq(movedToVoteTokenCount, 0); + + // Verify proposal type data + for (uint256 i = 0; i < proposalTypes.length; i++) { + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); + if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { + assertEq(requiredApprovals, 0); + } else { + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + } + + // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID + if ( + proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections + ) { + assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); + } else { + // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID + assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); + } + } + } + + function test_initialize_mismatchedArrayLengths_reverts() public { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + + // Create mismatched array with different length + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 1 + }); + + vm.prank(owner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + proposalTypesConfigurator, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); } } -/// @title ProposalValidator_ApproveProposal_TestFail -/// @notice Sad path tests for approveProposal function -contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { +/// @title ProposalValidator_SubmitUpgradeProposal_Test +/// @notice Happy path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { + string proposalDescription; + function setUp() public override { super.setUp(); - } - function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { - // There is no stored proposal data so this will revert - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + + proposalDescription = "Protocol Upgrade Proposal"; } - function test_approveProposal_proposalAlreadyApproved_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue + function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( + uint248 againstThreshold, + address proposer ) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); - // Mock the proposal as already approved by the top delegate - validator.mockApproveProposal(_proposalHash, topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; - function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); - // create a new schema - vm.prank(topDelegate_A); - bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "string top100, string date", ISchemaResolver(address(0)), true - ); + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // create an attestation with the new schema - vm.prank(topDelegate_A); - bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: _invalidSchemaUid, - data: AttestationRequestData({ - recipient: topDelegate_A, - expirationTime: 0, - revocable: true, - refUID: bytes32(0), - data: abi.encode("top100", false, "2000-01-01"), - value: 0 - }) - }) + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _invalidAttestationUid); - } + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); - function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) - }) + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) ); - vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } - - function test_approveProposal_invalidAttestationCaller_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue, - address _caller - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Ensure the caller is not a top delegate - vm.assume( - _caller != topDelegate_A && _caller != topDelegate_B && _caller != topDelegate_C && _caller != topDelegate_D - ); + // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(_caller); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, proposer); - function test_approveProposal_invalidAttestationPartialDelegation_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + vm.prank(proposer); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + assertEq(proposalHash, expectedHash); - // create an attestation with partial delegation - vm.prank(owner); - bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: topDelegate_A, - expirationTime: 0, - revocable: true, - refUID: bytes32(0), - data: abi.encode("top100", true, "2000-01-01"), - value: 0 - }) - }) - ); + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } - function test_approveProposal_nonExistentAttestation_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue, - bytes32 _nonExistentAttestationUid + function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( + uint248 againstThreshold, + address proposer ) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Ensure the attestation uid is not one of the valid ones - vm.assume( - _nonExistentAttestationUid != topDelegateAttestation_A - && _nonExistentAttestationUid != topDelegateAttestation_B - && _nonExistentAttestationUid != topDelegateAttestation_C - && _nonExistentAttestationUid != topDelegateAttestation_D - ); - - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); - // Expect the invalid attestation error to be reverted when attestation doesn't exist - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _nonExistentAttestationUid); - } -} + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes votingModuleData; - bytes32 expectedHash; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - function setUp() public override { - super.setUp(); + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( - approvedProposer, againstThreshold, proposalDescription + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); - // Move to vote - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); - assertTrue(movedToVote, "Proposal should be in voting"); + vm.prank(proposer); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes votingModuleData; - bytes32 expectedHash; +/// @title ProposalValidator_SubmitUpgradeProposal_TestFail +/// @notice Sad path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { + string proposalDescription; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( - approvedProposer, againstThreshold, proposalDescription - ); + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + + proposalDescription = "Test upgrade proposal"; } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) - public - { - vm.assume(_caller != approvedProposer); + function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) + proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; // 50% + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts( - uint248 _againstThreshold - ) - public - { - // This will generate a different proposal hash which will make the proposal type wrong - vm.assume(_againstThreshold != againstThreshold); + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - - // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { + uint248 zeroThreshold = 0; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { + // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR + excessiveThreshold = + uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } -} -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; - uint128 criteriaValue = 1; - bytes32 expectedHash; - bytes votingModuleData; - string proposalDescription = "Test proposal"; - string[] optionsDescriptions = new string[](2); + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function setUp() public override { - super.setUp(); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Create a proposal for move to vote with 1 top choice and 2 options - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - proposalDescription + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + // For MaintenanceUpgrade, mock the governor.proposeWithModule call + if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + } - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); - assertTrue(movedToVote, "Proposal should be in voting"); - } -} + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; - uint128 criteriaValue = 1; - string proposalDescription = "Test proposal"; - string[] optionsDescriptions = new string[](2); - bytes32 expectedHash; - bytes votingModuleData; + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - function setUp() public override { - super.setUp(); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Create a proposal for move to vote with 1 top choice and 2 options - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - proposalDescription + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); } - function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) - public - { - vm.assume(_caller != approvedProposer); + function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); - } + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { - // This will generate a different proposal hash which will make the proposal type wrong - uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, proposalType), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { - // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); - } + // Create valid attestation first (make it revocable) + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: attestationUid, value: 0 }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(block.number + DURATION + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } +} - function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test +/// @notice Happy path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + string proposalDescription; - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + function setUp() public override { + super.setUp(); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) - ); + _setCouncilMemberElectionsProposalType(); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + proposalDescription = "Council Member Elections Q4 2024"; } -} -contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { - ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; - ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; - uint128 criteriaValue = 1; - string governanceFundProposalDescription = "Test governance fund proposal"; - string councilBudgetProposalDescription = "Test council budget proposal"; - string[] optionsDescriptions = new string[](2); - address[] optionsRecipients = new address[](2); - uint256[] optionsAmounts = new uint256[](2); - bytes32 expectedGovernanceFundHash; - bytes32 expectedCouncilBudgetHash; - bytes governanceFundVotingModuleData; - bytes councilBudgetVotingModuleData; - - function setUp() public override { - super.setUp(); - - // Create option descriptions for the proposals - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; + function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { + optionCount = uint8(bound(optionCount, 2, 5)); // Minimum 2 options to have valid criteria < optionCount + criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount - // Create option recipients for the proposals - optionsRecipients[0] = makeAddr("optionRecipient1"); - optionsRecipients[1] = makeAddr("optionRecipient2"); + // Create dynamic array of option descriptions based on option count + string[] memory optionDescriptions = new string[](optionCount); + for (uint256 i = 0; i < optionCount; i++) { + optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); + } - // Create option amounts for the proposals - optionsAmounts[0] = 100 ether; - optionsAmounts[1] = 200 ether; + // Create attestation for the proposal + bytes32 attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Create one proposal for each type - (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType - ); - (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteFundingProposal_governanceFund_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - ( - VotingModule(approvalVotingModule), - governanceFundVotingModuleData, - governanceFundProposalDescription, - uint8(governanceFundProposalType) - ) - ), - abi.encode(uint256(expectedGovernanceFundHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted + // Expect ProposalSubmitted event vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); - - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType + emit ProposalSubmitted( + expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections ); - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); - assertTrue(movedToVote, "Proposal should be in voting"); - } + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - function test_moveToVoteFundingProposal_councilBudget_succeeds() public { - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - ( - VotingModule(approvalVotingModule), - councilBudgetVotingModuleData, - councilBudgetProposalDescription, - uint8(councilBudgetProposalType) - ) - ), - abi.encode(uint256(expectedCouncilBudgetHash)) + vm.prank(topDelegate_A); + bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + assertEq(proposalHash, expectedHash); - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType + // Verify proposal data was stored correctly + ( + address proposer, + ProposalValidator.ProposalType proposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); + + assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); + assertEq( + uint8(proposalType), + uint8(ProposalValidator.ProposalType.CouncilMemberElections), + "Proposal type should be CouncilMemberElections" ); + assertFalse(movedToVote, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } -contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; - ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; - uint128 criteriaValue = 1; - string governanceFundProposalDescription = "Test governance fund proposal"; - string councilBudgetProposalDescription = "Test council budget proposal"; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - bytes32 governanceFundExpectedHash; - bytes32 councilBudgetExpectedHash; - bytes governanceFundVotingModuleData; - bytes councilBudgetVotingModuleData; +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail +/// @notice Sad path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionDescriptions; + string proposalDescription; + bytes32 attestationUid; function setUp() public override { super.setUp(); - (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); - (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType - ); - (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType - ); + _setCouncilMemberElectionsProposalType(); + + criteriaValue = 2; + optionDescriptions = new string[](3); + optionDescriptions[0] = "Candidate A"; + optionDescriptions[1] = "Candidate B"; + optionDescriptions[2] = "Candidate C"; + + proposalDescription = "Test Council Elections"; + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } - function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( - uint8 _proposalTypeValue, - string memory _proposalDescription - ) + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidProposal_reverts( - uint8 _proposalTypeValue, - uint128 _criteriaValue - ) - public - { - // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will - // not find the proposal - vm.assume(_criteriaValue != criteriaValue); - - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { + string[] memory emptyOptions = new string[](0); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( - uint8 _wrongProposalTypeValue, - uint8 _validProposalTypeValue - ) - public - { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); - ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); - - _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); - ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); + function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - string memory proposalDescription; - if (validProposalType == governanceFundProposalType) { - // Set proposal data proposal type to a different value - validator.setProposalData(governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data proposal type to a different value - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - proposalDescription, - validProposalType + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - } - - function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - // Set proposal data movedToVote to true - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data movedToVote to true - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) + public + { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + value: 0 + }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(START_BLOCK + DURATION + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( - uint8 _proposalTypeValue + function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( + uint128 invalidCriteriaValue ) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } - - // Set the first option amount to exceed the distribution threshold - optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + // Bound invalidCriteriaValue to be greater than options length + invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); - vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } - - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - - string[] memory _optionsDescriptions = new string[](3); - address[] memory _optionsRecipients = new address[](3); - uint256[] memory _optionsAmounts = new uint256[](3); + function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { + // Create valid attestation first (make it revocable) + bytes32 revocableAttestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - _optionsDescriptions[0] = "Option 1"; - _optionsDescriptions[1] = "Option 2"; - _optionsDescriptions[2] = "Option 3"; + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) + }) + ); - _optionsRecipients[0] = makeAddr("optionRecipient1"); - _optionsRecipients[1] = makeAddr("optionRecipient2"); - _optionsRecipients[2] = makeAddr("optionRecipient3"); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER + ); + } +} - _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; +/// @title ProposalValidator_SubmitFundingProposal_Test +/// @notice Happy path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { + uint128 criteriaValue = 1000 ether; + string description = "Test funding proposal"; - _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - _optionsDescriptions, - _optionsRecipients, - _optionsAmounts, - proposalDescription, - proposalType - ); + function setUp() public override { + super.setUp(); - vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(approvedProposer); - vm.roll(START_BLOCK + 1); - validator.moveToVoteFundingProposal( - criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType - ); + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); } - function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( - uint8 _proposalTypeValue, - bytes32 _randomHash + function testFuzz_submitFundingProposal_succeeds( + uint8 proposalTypeValue, + uint8 optionCount, + uint256 amount, + address proposer ) public { - vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - - bytes memory votingModuleData; - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - votingModuleData = governanceFundVotingModuleData; - proposalDescription = governanceFundProposalDescription; - } else { - votingModuleData = councilBudgetVotingModuleData; - proposalDescription = councilBudgetProposalDescription; - } - - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) - ); - - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType - ); - } -} - -/// @title ProposalValidator_CanApproveProposal_Test -/// @notice Tests for the canApproveProposal function -contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public { - // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); - assertTrue(canApprove); - } - - function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { - // Ensure the attestation uid is not one of the top delegates - vm.assume( - attestationUid != topDelegateAttestation_A && attestationUid != topDelegateAttestation_B - && attestationUid != topDelegateAttestation_C && attestationUid != topDelegateAttestation_D - ); - - bool canApprove; - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { - canApprove = result_; - } catch { - canApprove = false; - } - - assertEq(canApprove, false); - } -} - -/// @title ProposalValidator_Version_Test -/// @notice Tests for the version function -contract ProposalValidator_Version_Test is ProposalValidator_Init { - function test_version_succeeds() public { - string memory versionString = validator.version(); - assertEq(versionString, "1.0.0-beta.1"); - } -} - -/// @title ProposalValidator_Setters_Test -/// @notice Tests for setter functions -contract ProposalValidator_Setters_Test is ProposalValidator_Init { - function testFuzz_setVotingCycleData_succeeds( - uint256 cycleNumber, - uint256 startBlock, - uint256 duration, - uint256 distributionLimit - ) - public - { - vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle - - // Expect the VotingCycleDataSet event to be emitted - vm.expectEmit(address(validator)); - emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); - - vm.prank(owner); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - - ( - uint256 actualStartBlock, - uint256 actualDuration, - uint256 actualDistributionLimit, - uint256 actualMovedToVoteTokenCount - ) = validator.votingCycles(cycleNumber); - - assertEq(actualStartBlock, startBlock); - assertEq(actualDuration, duration); - assertEq(actualDistributionLimit, distributionLimit); - assertEq(actualMovedToVoteTokenCount, 0); - } - - function test_setVotingCycleData_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - } - - function test_setVotingCycleData_votingCycleAlreadySet_reverts() public { - vm.prank(owner); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - - vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); - vm.prank(owner); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - } - - function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { - // Expect the DistributionThresholdSet event to be emitted - vm.expectEmit(address(validator)); - emit DistributionThresholdSet(newDistributionThreshold); - - vm.prank(owner); - validator.setDistributionThreshold(newDistributionThreshold); - - assertEq(validator.distributionThreshold(), newDistributionThreshold); - } - - function test_setDistributionThreshold_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setDistributionThreshold(10000 ether); - } - - function testFuzz_setProposalTypeData_succeeds( - uint8 proposalTypeValue, - uint256 newRequiredApprovals, - uint8 newProposalTypeId - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ - requiredApprovals: newRequiredApprovals, - proposalVotingModule: newProposalTypeId - }); - - // Expect the ProposalTypeDataSet event to be emitted - vm.expectEmit(address(validator)); - emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); - - vm.prank(owner); - validator.setProposalTypeData(proposalType, newData); - - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); - assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newProposalTypeId); - } - - function test_setProposalTypeData_notOwner_reverts() public { - ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); - } -} - -/// @title ProposalValidator_HashProposalWithModule_Test -/// @notice Tests for the hashProposalWithModule function -contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { - function test_hashProposalWithModule_succeeds() public { - address testModule = makeAddr("testModule"); - bytes memory testProposalData = abi.encode("test", "proposal", "data"); - bytes32 testDescriptionHash = keccak256("test description"); - - bytes32 hash = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - assertTrue(hash != bytes32(0)); - } - - function test_hashProposalWithModule_consistentHash_succeeds() public { - address testModule = makeAddr("testModule"); - bytes memory testProposalData = abi.encode("test data"); - bytes32 testDescriptionHash = keccak256("description"); - - bytes32 hash1 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - bytes32 hash2 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - - assertEq(hash1, hash2); - } - - function test_hashProposalWithModule_differentInputs_succeeds() public { - address module1 = makeAddr("module1"); - address module2 = makeAddr("module2"); - bytes memory data = abi.encode("data"); - bytes32 descHash = keccak256("desc"); - - bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); - bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); - - assertTrue(hash1 != hash2); - } -} - -/// @title ProposalValidator_SubmitFundingProposal_Test -/// @notice Happy path tests for submitFundingProposal function -contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - string description; - - function setUp() public override { - super.setUp(); - - _setFundingProposalTypes(); - - criteriaValue = 1000 ether; - } - - function testFuzz_submitFundingProposal_succeeds( - uint8 proposalTypeValue, - uint8 optionCount, - uint256 amount, - address proposer - ) - public - { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Bound option count between 1 and 50 for reasonable test execution - optionCount = uint8(bound(optionCount, 1, 50)); + // Bound option count between 1 and 5 for reasonable test execution + optionCount = uint8(bound(optionCount, 1, 5)); // Bound amount from 0 to DISTRIBUTION_THRESHOLD (inclusive) amount = bound(amount, 0, DISTRIBUTION_THRESHOLD); - // Create arrays based on option count - string[] memory descriptions = new string[](optionCount); - address[] memory recipients = new address[](optionCount); - uint256[] memory amounts = new uint256[](optionCount); + // Start with minimal arrays and extend based on option count + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); for (uint256 i = 0; i < optionCount; i++) { - descriptions[i] = string(abi.encodePacked("Option ", vm.toString(i))); - recipients[i] = makeAddr(string(abi.encodePacked("recipient", vm.toString(i)))); - amounts[i] = amount; // Use the same bounded amount for all options + descriptions[i] = descriptions[0]; + recipients[i] = recipients[0]; + amounts[i] = amount; } // Calculate expected proposal hash @@ -1824,7 +1450,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I function setUp() public override { super.setUp(); // Set both funding proposal types to use the approval voting module - _setFundingProposalTypes(); + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { @@ -2090,752 +1717,1107 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I } } -/// @title ProposalValidator_Initialize_Test -/// @notice Tests for the initialize function -contract ProposalValidator_Initialize_Test is ProposalValidator_Init { - /// @dev Override to create validator proxy without initialization for testing - function _initializeValidator() internal override { - // Create mock addresses - proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); +/// @title ProposalValidator_ApproveProposal_Test +/// @notice Happy path tests for approveProposal function +contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Ensure the proposal hash is not 0 + vm.assume(_proposalHash != bytes32(0)); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken - ); - validator = ProposalValidatorForTest(address(new Proxy(owner))); - } + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function test_initialize_succeeds() public { - ( - ProposalValidator.ProposalType[] memory proposalTypes, - ProposalValidator.ProposalTypeData[] memory proposalTypesData - ) = _getProposalTypesAndData(); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - vm.prank(owner); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - proposalTypesConfigurator, - CYCLE_NUMBER, - START_BLOCK, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) - ); + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(_proposalHash, topDelegate_A); - // Verify initialization was successful - assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); - assertEq(validator.owner(), owner); + // Approve the proposal, use the attestation of the top delegate that was created in setUp + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); - // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = - validator.votingCycles(CYCLE_NUMBER); - assertEq(startBlock, START_BLOCK); - assertEq(duration, DURATION); - assertEq(distributionLimit, DISTRIBUTION_LIMIT); - assertEq(movedToVoteTokenCount, 0); + // Check that the proposal data has been updated + assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - // Verify proposal type data - for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); - if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { - assertEq(requiredApprovals, 0); - } else { - assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - } + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); + assertEq(approvalCount, 1); + } +} - // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID - if ( - proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections - ) { - assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); - } else { - // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID - assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); - } - } +/// @title ProposalValidator_ApproveProposal_TestFail +/// @notice Sad path tests for approveProposal function +contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { + function setUp() public override { + super.setUp(); } - function test_initialize_mismatchedArrayLengths_reverts() public { - ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); - proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; - proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + // There is no stored proposal data so this will revert + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } - // Create mismatched array with different length - ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); - proposalTypesData[0] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 - }); - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 - }); + function test_approveProposal_proposalAlreadyApproved_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal as already approved by the top delegate + validator.mockApproveProposal(_proposalHash, topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // create a new schema + vm.prank(topDelegate_A); + bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100, string date", ISchemaResolver(address(0)), true + ); + + // create an attestation with the new schema + vm.prank(topDelegate_A); + bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: _invalidSchemaUid, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); + + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _invalidAttestationUid); + } + function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + // revoke the attestation vm.prank(owner); - vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - proposalTypesConfigurator, - CYCLE_NUMBER, - START_BLOCK, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) + }) ); + + vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} -/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test -/// @notice Happy path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { - string proposalDescription; + function test_approveProposal_invalidAttestationCaller_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + address _caller + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function setUp() public override { - super.setUp(); + // Ensure the caller is not a top delegate + vm.assume(_caller != topDelegate_A); - _setCouncilMemberElectionsProposalType(); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = "Council Member Elections Q4 2024"; + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(_caller); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { - optionCount = uint8(bound(optionCount, 2, type(uint8).max)); // Minimum 2 options to have valid criteria < - // optionCount - criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount - - // Create dynamic array of option descriptions based on option count - string[] memory optionDescriptions = new string[](optionCount); - for (uint256 i = 0; i < optionCount; i++) { - optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); - } + function test_approveProposal_invalidAttestationPartialDelegation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Create attestation for the proposal - bytes32 attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + // create an attestation with partial delegation + vm.prank(owner); + bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", true, "2000-01-01"), + value: 0 + }) + }) ); - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) - ); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + } - // Expect ProposalSubmitted event - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections - ); + function test_approveProposal_nonExistentAttestation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + bytes32 _nonExistentAttestationUid + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Expect ProposalVotingModuleData event - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Ensure the attestation uid is not one of the valid ones + vm.assume(_nonExistentAttestationUid != topDelegateAttestation_A); - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Expect the invalid attestation error to be reverted when attestation doesn't exist + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + } +} - assertEq(proposalHash, expectedHash); +/// @title ProposalValidator_CanApproveProposal_Test +/// @notice Tests for the canApproveProposal function +contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { + function test_canApproveProposal_returnTrue_succeeds() public { + // Attestation already created in setUp + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + assertTrue(canApprove); + } - // Verify proposal data was stored correctly - ( - address proposer, - ProposalValidator.ProposalType proposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { + // Ensure the attestation uid is not one of the top delegates + vm.assume(attestationUid != topDelegateAttestation_A); - assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); - assertEq( - uint8(proposalType), - uint8(ProposalValidator.ProposalType.CouncilMemberElections), - "Proposal type should be CouncilMemberElections" - ); - assertFalse(movedToVote, "Proposal should not be in voting yet"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + bool canApprove; + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { + canApprove = result_; + } catch { + canApprove = false; + } + + assertEq(canApprove, false); } } -/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail -/// @notice Sad path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionDescriptions; - string proposalDescription; - bytes32 attestationUid; +/// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test +/// @notice Happy path tests for moveToVoteProtocolOrGovernorUpgradeProposal function +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = + _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; function setUp() public override { super.setUp(); - _setCouncilMemberElectionsProposalType(); + (expectedHash, votingModuleData) = + _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); + } - criteriaValue = 2; - optionDescriptions = new string[](3); - optionDescriptions[0] = "Candidate A"; - optionDescriptions[1] = "Candidate B"; - optionDescriptions[2] = "Candidate C"; + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) public { + vm.assume(_caller != approvedProposer); - proposalDescription = "Test Council Elections"; - attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 _againstThreshold) public { - vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation + // This will generate a different proposal hash which will make the proposal type wrong + vm.assume(_againstThreshold != againstThreshold); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { - vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - // Try to submit with different address than attested - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { - string[] memory emptyOptions = new string[](0); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); - // Mock proposalSnapshot to return 0 for first submission + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) ); - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } +} - // Submit first proposal - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + bytes32 expectedHash; + bytes votingModuleData; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); - // Create new attestation for second attempt - bytes32 secondAttestation = - _createApprovedProposerAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + function setUp() public override { + super.setUp(); - // Attempt to submit identical proposal should revert - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, criteriaValue, optionsDescriptions, proposalDescription + ); + } + function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { + // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(topDelegate_B); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, secondAttestation, CYCLE_NUMBER + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); } +} - function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + bytes32 expectedHash; + bytes votingModuleData; - // Mock proposalSnapshot to return non-zero (proposal already exists in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(1000) // Non-zero indicates proposal exists + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); + } - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) public { + vm.assume(_caller != approvedProposer); + // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) - public - { - vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { + // This will generate a different proposal hash which will make the proposal type wrong + uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp - // Create attestation but don't use proper owner as attester - vm.prank(fuzzedAttester); // Not the owner - bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, - revocable: false, - refUID: bytes32(0), - data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), - value: 0 - }) - }) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER - ); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( - uint128 invalidCriteriaValue - ) - public - { - // Bound invalidCriteriaValue to be greater than options length - invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(block.number + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { - // Create valid attestation first (make it revocable) - bytes32 revocableAttestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); - // Revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) - }) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } } -/// @title ProposalValidator_SubmitUpgradeProposal_Test -/// @notice Happy path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { - string proposalDescription; +contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions = new string[](2); + address[] optionsRecipients = new address[](2); + uint256[] optionsAmounts = new uint256[](2); + bytes32 expectedGovernanceFundHash; + bytes32 expectedCouncilBudgetHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; function setUp() public override { super.setUp(); - _setUpgradeProposalTypes(); - - proposalDescription = "Protocol Upgrade Proposal"; - } - - function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( - uint248 againstThreshold, - address proposer - ) - public - { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + // Create option descriptions for the proposals + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Create option recipients for the proposals + optionsRecipients[0] = makeAddr("optionRecipient1"); + optionsRecipients[1] = makeAddr("optionRecipient2"); - // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + // Create option amounts for the proposals + optionsAmounts[0] = 100 ether; + optionsAmounts[1] = 200 ether; - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + // Create one proposal for each type + (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType ); + } - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + function test_moveToVoteFundingProposal_governanceFund_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + governanceFundVotingModuleData, + governanceFundProposalDescription, + uint8(governanceFundProposalType) + ) + ), + abi.encode(uint256(expectedGovernanceFundHash)) ); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } + + function test_moveToVoteFundingProposal_councilBudget_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ( + VotingModule(approvalVotingModule), + councilBudgetVotingModuleData, + councilBudgetProposalDescription, + uint8(councilBudgetProposalType) + ) ), - abi.encode(uint256(expectedHash)) + abi.encode(uint256(expectedCouncilBudgetHash)) ); - // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } +} - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, proposer); +contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + bytes32 governanceFundExpectedHash; + bytes32 councilBudgetExpectedHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; - vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + function setUp() public override { + super.setUp(); + + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType ); + } - assertEq(proposalHash, expectedHash); + function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( + uint8 _proposalTypeValue, + string memory _proposalDescription + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // Verify proposal data was stored correctly - ( - address storedProposer, - ProposalValidator.ProposalType storedProposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposal_reverts( + uint8 _proposalTypeValue, + uint128 _criteriaValue + ) + public + { + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // not find the proposal + vm.assume(_criteriaValue != criteriaValue); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - assertEq(storedProposer, proposer, "Proposer should match input"); - assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } - function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( - uint248 againstThreshold, - address proposer + function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( + uint8 _wrongProposalTypeValue, + uint8 _validProposalTypeValue ) public { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); - // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + string memory proposalDescription; + if (validProposalType == governanceFundProposalType) { + // Set proposal data proposal type to a different value + validator.setProposalData( + governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data proposal type to a different value + validator.setProposalData( + councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = councilBudgetProposalDescription; + } - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + proposalDescription, + validProposalType ); + } - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events - vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData( + governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); + } - assertEq(proposalHash, expectedHash); + function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // Verify proposal data was stored correctly - ( - address storedProposer, - ProposalValidator.ProposalType storedProposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data movedToVote to true + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data movedToVote to true + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } - assertEq(storedProposer, proposer, "Proposer should match input"); - assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } -} -/// @title ProposalValidator_SubmitUpgradeProposal_TestFail -/// @notice Sad path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { - string proposalDescription; + function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - function setUp() public override { - super.setUp(); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - _setUpgradeProposalTypes(); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - proposalDescription = "Test upgrade proposal"; + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(START_BLOCK + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } - function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { - // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) - proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + uint8 _proposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - uint248 againstThreshold = 5000; // 50% - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + // Set the first option amount to exceed the distribution threshold + optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); } - function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER - ); - } + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { - vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + string[] memory _optionsDescriptions = new string[](3); + address[] memory _optionsRecipients = new address[](3); + uint256[] memory _optionsAmounts = new uint256[](3); - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + _optionsDescriptions[0] = "Option 1"; + _optionsDescriptions[1] = "Option 2"; + _optionsDescriptions[2] = "Option 3"; - // Try to submit with different address than attested - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + _optionsRecipients[0] = makeAddr("optionRecipient1"); + _optionsRecipients[1] = makeAddr("optionRecipient2"); + _optionsRecipients[2] = makeAddr("optionRecipient3"); + + _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; + + _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + _optionsDescriptions, + _optionsRecipients, + _optionsAmounts, + proposalDescription, + proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + vm.roll(START_BLOCK + 1); + validator.moveToVoteFundingProposal( + criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType ); } - function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { - uint248 zeroThreshold = 0; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( + uint8 _proposalTypeValue, + bytes32 _randomHash + ) + public + { + vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); - } + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { - // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR - excessiveThreshold = - uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); + bytes memory votingModuleData; + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + votingModuleData = governanceFundVotingModuleData; + proposalDescription = governanceFundProposalDescription; + } else { + votingModuleData = councilBudgetVotingModuleData; + proposalDescription = councilBudgetProposalDescription; + } - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); } +} - function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_Setters_Test +/// @notice Tests for setter functions +contract ProposalValidator_Setters_Test is ProposalValidator_Init { + function testFuzz_setVotingCycleData_succeeds( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle - uint248 againstThreshold = 5000; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + // Expect the VotingCycleDataSet event to be emitted + vm.expectEmit(address(validator)); + emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + vm.prank(owner); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - // Mock proposalSnapshot to return 0 for first submission - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) - ); + ( + uint256 actualStartBlock, + uint256 actualDuration, + uint256 actualDistributionLimit, + uint256 actualMovedToVoteTokenCount + ) = validator.votingCycles(cycleNumber); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + assertEq(actualStartBlock, startBlock); + assertEq(actualDuration, duration); + assertEq(actualDistributionLimit, distributionLimit); + assertEq(actualMovedToVoteTokenCount, 0); + } - // For MaintenanceUpgrade, mock the governor.proposeWithModule call - if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) - ); - } + function testFuzz_setVotingCycleData_notOwner_reverts( + address caller, + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(caller != owner); - // Submit first proposal - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER - ); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + } - // Create new attestation for second attempt - bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); + function testFuzz_setVotingCycleData_votingCycleAlreadySet_reverts( + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER, startBlock, duration, distributionLimit); + } - // Attempt to submit identical proposal should revert - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { + // Expect the DistributionThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit DistributionThresholdSet(newDistributionThreshold); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.prank(owner); + validator.setDistributionThreshold(newDistributionThreshold); - vm.prank(topDelegate_B); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, secondAttestation, proposalType, CYCLE_NUMBER - ); + assertEq(validator.distributionThreshold(), newDistributionThreshold); } - function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + function testFuzz_setDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { + vm.assume(caller != owner); - uint248 againstThreshold = 5000; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setDistributionThreshold(threshold); + } - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + function testFuzz_setProposalTypeData_succeeds( + uint8 proposalTypeValue, + uint256 newRequiredApprovals, + uint8 newProposalTypeId + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock proposalSnapshot to return non-zero (proposal already exists in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(1000) // Non-zero indicates proposal exists - ); + ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ + requiredApprovals: newRequiredApprovals, + proposalVotingModule: newProposalTypeId + }); - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + // Expect the ProposalTypeDataSet event to be emitted + vm.expectEmit(address(validator)); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.prank(owner); + validator.setProposalTypeData(proposalType, newData); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER - ); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); + assertEq(requiredApprovals, newRequiredApprovals); + assertEq(proposalVotingModule, newProposalTypeId); } - function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { - vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + function testFuzz_setProposalTypeData_notOwner_reverts(address caller) public { + vm.assume(caller != owner); - // Create attestation but don't use proper owner as attester - vm.prank(fuzzedAttester); // Not the owner - bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, - revocable: false, - refUID: bytes32(0), - data: abi.encode(topDelegate_A, proposalType), - value: 0 - }) - }) - ); + ProposalValidator.ProposalTypeData memory newData = + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER - ); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } +} - function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_HashProposalWithModule_Test +/// @notice Tests for the hashProposalWithModule function +contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { + function testFuzz_hashProposalWithModule_succeeds( + address module, + bytes memory proposalData, + bytes32 descriptionHash + ) + public + { + bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); + bytes32 expectedHash = + keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash)); - uint248 againstThreshold = 5000; + assertEq(hash, expectedHash); + } - // Create valid attestation first (make it revocable) - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_hashProposalWithModule_differentInputs_succeeds() public { + address module1 = makeAddr("module1"); + address module2 = makeAddr("module2"); + bytes memory data = abi.encode("data"); + bytes32 descHash = keccak256("desc"); - // Revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: attestationUid, value: 0 }) - }) - ); + bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); + bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); - vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); + assertTrue(hash1 != hash2); } } From 019d7902aab41fa5ec675331b7e7e654541c48e0 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:48:36 +0300 Subject: [PATCH 19/73] fix: voting window to use timestamp (#442) * fix: voting window to use timestamp * fix: pre-pr * fix: improve test --- .../governance/IProposalValidator.sol | 10 +-- .../snapshots/abi/ProposalValidator.json | 18 +++- .../src/governance/ProposalValidator.sol | 51 ++++++----- .../test/governance/ProposalValidator.t.sol | 89 +++++++++---------- 4 files changed, 90 insertions(+), 78 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 2ba27a28d83..1693007ce3a 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -51,7 +51,7 @@ interface IProposalValidator is ISemver { event VotingCycleDataSet( uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); @@ -88,7 +88,7 @@ interface IProposalValidator is ISemver { } struct VotingCycleData { - uint256 startingBlock; + uint256 startingTimestamp; uint256 duration; uint256 votingCycleDistributionLimit; uint256 movedToVoteTokenCount; @@ -154,7 +154,7 @@ interface IProposalValidator is ISemver { function setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) external; @@ -170,7 +170,7 @@ interface IProposalValidator is ISemver { address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, @@ -203,7 +203,7 @@ interface IProposalValidator is ISemver { function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( - uint256 startingBlock, + uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit, uint256 movedToVoteTokenCount diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 4af4cf80adf..40ba0366dd0 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -177,7 +177,7 @@ }, { "internalType": "uint256", - "name": "_startBlock", + "name": "_startingTimestamp", "type": "uint256" }, { @@ -429,7 +429,7 @@ }, { "internalType": "uint256", - "name": "_startBlock", + "name": "_startingTimestamp", "type": "uint256" }, { @@ -613,7 +613,7 @@ "outputs": [ { "internalType": "uint256", - "name": "startingBlock", + "name": "startingTimestamp", "type": "uint256" }, { @@ -805,7 +805,7 @@ { "indexed": false, "internalType": "uint256", - "name": "startBlock", + "name": "startingTimestamp", "type": "uint256" }, { @@ -869,6 +869,16 @@ "name": "ProposalValidator_InvalidOptionsLength", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidProposal", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidProposer", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidUpgradeProposalType", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index a9559e32ba6..1aa237ef4be 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -119,11 +119,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. - /// @param startBlock The block number of the starting block of the voting cycle. + /// @param startingTimestamp The starting timestamp of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); /// @notice Emitted when the distribution threshold is set. @@ -171,12 +171,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Struct for storing voting cycle data. - /// @param startingBlock The block number of the starting block of the voting cycle. - /// @param duration The duration of the voting cycle. + /// @param startingTimestamp The starting timestamp of the voting cycle. + /// @param duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week 3 + /// of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. /// @param movedToVoteTokenCount The total amount of tokens to possibly be distributed in the voting cycle. struct VotingCycleData { - uint256 startingBlock; + uint256 startingTimestamp; uint256 duration; uint256 votingCycleDistributionLimit; uint256 movedToVoteTokenCount; @@ -273,7 +274,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _owner The address that will own the contract. /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _cycleNumber The number of the current voting cycle. - /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. @@ -283,7 +284,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, @@ -298,7 +299,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposalTypesConfigurator = _proposalTypesConfigurator; - _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { @@ -712,10 +713,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if the voting cycle is valid VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - // TODO: is + duration correct? if ( - votingCycleData.startingBlock > block.number - || votingCycleData.startingBlock + votingCycleData.duration < block.number + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp ) { revert ProposalValidator_InvalidVotingCycle(); } @@ -805,10 +805,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if proposal can be moved to vote VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - // TODO: is + duration correct? if ( - votingCycleData.startingBlock > block.number - || votingCycleData.startingBlock + votingCycleData.duration < block.number + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp ) { revert ProposalValidator_InvalidVotingCycle(); } @@ -837,19 +836,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Sets the data of a voting cycle. /// @param _cycleNumber The number of the voting cycle to set. - /// @param _startBlock The block number of the starting block of the voting cycle. - /// @param _duration The duration of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. + /// @param _duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week + /// 3 + /// of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. function setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) external onlyOwner { - _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } /// @notice Sets the max amount of tokens that can be distributed in a proposal. @@ -1021,28 +1022,30 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Private function to set the voting cycle data and emit event. /// @param _cycleNumber The number of the voting cycle to set. - /// @param _startBlock The block number of the starting block of the voting cycle. - /// @param _duration The duration of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. + /// @param _duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week + /// 3 + /// of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. function _setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) private { - if (votingCycles[_cycleNumber].startingBlock != 0) { + if (votingCycles[_cycleNumber].startingTimestamp != 0) { revert ProposalValidator_VotingCycleAlreadySet(); } votingCycles[_cycleNumber] = VotingCycleData({ - startingBlock: _startBlock, + startingTimestamp: _startingTimestamp, duration: _duration, votingCycleDistributionLimit: _votingCycleDistributionLimit, movedToVoteTokenCount: 0 }); - emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + emit VotingCycleDataSet(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } /// @notice Private function to set the distribution threshold and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fd369cf994c..6971a1de14b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -120,8 +120,8 @@ contract ProposalValidator_Init is CommonTest { using stdStorage for StdStorage; uint256 public constant CYCLE_NUMBER = 1; - uint256 public constant START_BLOCK = 1000000; - uint256 public constant DURATION = 100; + uint256 public constant START_TIMESTAMP = 1000000; + uint256 public constant DURATION = 1 days; uint256 public constant DISTRIBUTION_LIMIT = 20000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 1; @@ -155,7 +155,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); event MinimumVotingPowerSet(uint256 newMinimumVotingPower); event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalTypeDataSet( @@ -242,19 +242,18 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper to create minimal valid arrays for funding proposal error tests - function _createMinimalFundingArrays() + function _createMinimalFundingArrays(uint256 _length) internal - pure returns (string[] memory descriptions_, address[] memory recipients_, uint256[] memory amounts_) { - descriptions_ = new string[](1); - descriptions_[0] = "Option A"; - - recipients_ = new address[](1); - recipients_[0] = address(0x1); - - amounts_ = new uint256[](1); - amounts_[0] = 100 ether; + descriptions_ = new string[](_length); + recipients_ = new address[](_length); + amounts_ = new uint256[](_length); + for (uint256 i = 0; i < _length; i++) { + descriptions_[i] = string.concat("Option ", vm.toString(i + 1)); + recipients_[i] = makeAddr(string.concat("recipient", vm.toString(i + 1))); + amounts_[i] = 100 ether * (i + 1); + } } function _getProposalTypesAndData() @@ -516,7 +515,7 @@ contract ProposalValidator_Init is CommonTest { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -599,7 +598,7 @@ contract ProposalValidator_Init is CommonTest { /// @title ProposalValidator_Version_Test /// @notice Tests for the version function contract ProposalValidator_Version_Test is ProposalValidator_Init { - function test_version_succeeds() public { + function test_version_succeeds() public view { string memory versionString = validator.version(); assertEq(versionString, "1.0.0-beta.1"); } @@ -634,7 +633,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -649,9 +648,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { assertEq(validator.owner(), owner); // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + (uint256 startingTimestamp, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = validator.votingCycles(CYCLE_NUMBER); - assertEq(startBlock, START_BLOCK); + assertEq(startingTimestamp, START_TIMESTAMP); assertEq(duration, DURATION); assertEq(distributionLimit, DISTRIBUTION_LIMIT); assertEq(movedToVoteTokenCount, 0); @@ -706,7 +705,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -1386,11 +1385,10 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init // Start with minimal arrays and extend based on option count (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(optionCount); + // fuzz the amounts for (uint256 i = 0; i < optionCount; i++) { - descriptions[i] = descriptions[0]; - recipients[i] = recipients[0]; amounts[i] = amount; } @@ -1460,7 +1458,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); @@ -1586,7 +1584,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Create arrays with excessive amount (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); amounts[0] = excessAmount; vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); @@ -1602,7 +1600,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); // Calculate expected proposal hash bytes memory votingModuleData = @@ -1642,7 +1640,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); // Calculate expected proposal hash bytes memory votingModuleData = @@ -1921,7 +1919,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public { + function test_canApproveProposal_returnTrue_succeeds() public view { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); @@ -2111,7 +2109,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop emit ProposalMovedToVote(expectedHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); @@ -2192,7 +2190,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(block.number + DURATION + 1); + vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } @@ -2214,7 +2212,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } @@ -2294,7 +2292,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, @@ -2334,7 +2332,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, @@ -2364,7 +2362,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat function setUp() public override { super.setUp(); - (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(1); (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, @@ -2546,7 +2544,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(START_BLOCK + DURATION + 1); + vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType @@ -2622,7 +2620,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(approvedProposer); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); validator.moveToVoteFundingProposal( criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType ); @@ -2664,7 +2662,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType @@ -2677,7 +2675,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setVotingCycleData_succeeds( uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2687,19 +2685,19 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { // Expect the VotingCycleDataSet event to be emitted vm.expectEmit(address(validator)); - emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); + emit VotingCycleDataSet(cycleNumber, startingTimestamp, duration, distributionLimit); vm.prank(owner); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); ( - uint256 actualStartBlock, + uint256 actualStartingTimestamp, uint256 actualDuration, uint256 actualDistributionLimit, uint256 actualMovedToVoteTokenCount ) = validator.votingCycles(cycleNumber); - assertEq(actualStartBlock, startBlock); + assertEq(actualStartingTimestamp, startingTimestamp); assertEq(actualDuration, duration); assertEq(actualDistributionLimit, distributionLimit); assertEq(actualMovedToVoteTokenCount, 0); @@ -2708,7 +2706,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setVotingCycleData_notOwner_reverts( address caller, uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2718,11 +2716,11 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); } function testFuzz_setVotingCycleData_votingCycleAlreadySet_reverts( - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2730,7 +2728,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { { vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER, startBlock, duration, distributionLimit); + validator.setVotingCycleData(CYCLE_NUMBER, startingTimestamp, duration, distributionLimit); } function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { @@ -2801,6 +2799,7 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init bytes32 descriptionHash ) public + view { bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); bytes32 expectedHash = From a641388b4831c4ea905b254c0024a40272de3f69 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:13:28 +0300 Subject: [PATCH 20/73] fix: remove imported contracts (#443) * fix: voting window to use timestamp * fix: pre-pr * fix: improve test * fix: remove contracts and import interfaces * fix: snapshots * fix: remove unused state variable * fix: pre-pr * fix: change property naming --- .semgrep/rules/sol-rules.yaml | 19 - .../governance/IApprovalVotingModule.sol | 28 + .../governance/IOptimismGovernor.sol | 3 +- .../governance/IOptimisticModule.sol | 12 + .../governance/IProposalValidator.sol | 14 +- .../snapshots/abi/ApprovalVotingModule.json | 346 ------------ .../snapshots/abi/OptimisticModule.json | 258 --------- .../snapshots/abi/ProposalValidator.json | 76 +-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ApprovalVotingModule.json | 16 - .../storageLayout/OptimisticModule.json | 9 - .../storageLayout/ProposalValidator.json | 2 +- .../src/governance/ApprovalVotingModule.sol | 522 ------------------ .../src/governance/OptimisticModule.sol | 154 ------ .../src/governance/ProposalValidator.sol | 103 ++-- .../src/governance/VotingModule.sol | 73 --- .../test/governance/ProposalValidator.t.sol | 90 ++- 17 files changed, 161 insertions(+), 1568 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol create mode 100644 packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol delete mode 100644 packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json delete mode 100644 packages/contracts-bedrock/snapshots/abi/OptimisticModule.json delete mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json delete mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json delete mode 100644 packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol delete mode 100644 packages/contracts-bedrock/src/governance/OptimisticModule.sol delete mode 100644 packages/contracts-bedrock/src/governance/VotingModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index 3d24d5e8fae..8666267a21a 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -46,7 +46,6 @@ rules: paths: exclude: - packages/contracts-bedrock/test/dispute/WETH98.t.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-safety-natspec-semver-match languages: [generic] @@ -111,7 +110,6 @@ rules: exclude: - packages/contracts-bedrock/test - packages/contracts-bedrock/scripts - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-input-arg-fmt languages: [solidity] @@ -127,9 +125,6 @@ rules: - packages/contracts-bedrock/src/universal/WETH98.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol - packages/contracts-bedrock/src/governance/GovernanceToken.sol - - packages/contracts-bedrock/src/governance/VotingModule.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -144,8 +139,6 @@ rules: - packages/contracts-bedrock/test/safe-tools - packages/contracts-bedrock/scripts/libraries/Solarray.sol - packages/contracts-bedrock/scripts/interfaces/IGnosisSafe.sol - - packages/contracts-bedrock/src/governance/VotingModule.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-doc-comment languages: [solidity] @@ -155,8 +148,6 @@ rules: paths: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -178,7 +169,6 @@ rules: - packages/contracts-bedrock/src/cannon/MIPS2.sol - packages/contracts-bedrock/src/cannon/libraries/MIPSMemory.sol - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-malformed-revert languages: [solidity] @@ -195,7 +185,6 @@ rules: paths: exclude: - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-use-abi-encodecall languages: [solidity] @@ -212,7 +201,6 @@ rules: exclude: - packages/contracts-bedrock/src/L1/OPContractsManager.sol - packages/contracts-bedrock/src/legacy/L1ChugSplashProxy.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-enforce-require-msg languages: [solidity] @@ -224,7 +212,6 @@ rules: paths: exclude: - packages/contracts-bedrock/src/universal/WETH98.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-no-bare-imports languages: [solidity] @@ -234,7 +221,6 @@ rules: paths: exclude: - packages/contracts-bedrock/test - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-error-format languages: [generic] @@ -255,9 +241,6 @@ rules: - packages/contracts-bedrock/src/libraries/Blueprint.sol - packages/contracts-bedrock/src/dispute/lib/Errors.sol - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - - packages/contracts-bedrock/src/governance/VotingModule.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol @@ -359,9 +342,7 @@ rules: - packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol - packages/contracts-bedrock/src/governance/MintManager.sol - packages/contracts-bedrock/src/governance/ProposalValidator.sol - - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ProposalValidator.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - packages/contracts-bedrock/src/periphery/TransferOnion.sol - packages/contracts-bedrock/src/periphery/faucet/Faucet.sol - packages/contracts-bedrock/src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol b/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol new file mode 100644 index 00000000000..f17217e8730 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title IApprovalVotingModule +/// @notice Interface for the Approval Voting Module containing only the essential types +/// needed by the ProposalValidator contract. +interface IApprovalVotingModule { + struct ProposalOption { + uint256 budgetTokensSpent; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + } + + struct ProposalSettings { + uint8 maxApprovals; + uint8 criteria; + address budgetToken; + uint128 criteriaValue; + uint128 budgetAmount; + } + + enum PassingCriteria { + Threshold, + TopChoices + } +} diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index 994bb597df4..dfd1af85fed 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {VotingModule} from "src/governance/VotingModule.sol"; import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; interface IOptimismGovernor { @@ -14,7 +13,7 @@ interface IOptimismGovernor { ) external returns (uint256 proposalId); function proposeWithModule( - VotingModule module, + address module, bytes memory proposalData, string memory description, uint8 proposalType diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol b/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol new file mode 100644 index 00000000000..8b03a9986df --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title IOptimisticModule +/// @notice Interface for the Optimistic Module containing only the essential types +/// needed by the ProposalValidator contract. +interface IOptimisticModule { + struct ProposalSettings { + uint248 againstThreshold; + bool isRelativeToVotableSupply; + } +} \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 1693007ce3a..5c0c48a7be4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; // Interfaces -import {IGovernanceToken} from './IGovernanceToken.sol'; import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; @@ -56,7 +55,7 @@ interface IProposalValidator is ISemver { uint256 votingCycleDistributionLimit ); - event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( ProposalType proposalType, @@ -159,7 +158,7 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit ) external; - function setDistributionThreshold(uint256 _distributionThreshold) external; + function setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) external; function setProposalTypeData( ProposalType _proposalType, @@ -173,7 +172,7 @@ interface IProposalValidator is ISemver { uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, - uint256 _distributionThreshold, + uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -182,9 +181,7 @@ interface IProposalValidator is ISemver { function transferOwnership(address newOwner) external; - function distributionThreshold() external view returns (uint256); - - function VOTING_TOKEN() external view returns (IGovernanceToken); + function proposalDistributionThreshold() external view returns (uint256); function GOVERNOR() external view returns (IOptimismGovernor); @@ -212,7 +209,6 @@ interface IProposalValidator is ISemver { function __constructor__( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _votingToken + IOptimismGovernor _governor ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json deleted file mode 100644 index d531b81bb48..00000000000 --- a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json +++ /dev/null @@ -1,346 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_governor", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "COUNTING_MODE", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "PROPOSAL_DATA_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "VOTE_PARAMS_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - }, - { - "internalType": "uint256", - "name": "budgetTokensSpent", - "type": "uint256" - } - ], - "name": "_afterExecute", - "outputs": [], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint8", - "name": "support", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "weight", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "params", - "type": "bytes" - } - ], - "name": "_countVote", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - } - ], - "name": "_formatExecuteParams", - "outputs": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "calldatas", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - } - ], - "name": "_voteSucceeded", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "getAccountTotalVotes", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "getAccountVotes", - "outputs": [ - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "proposals", - "outputs": [ - { - "internalType": "address", - "name": "governor", - "type": "address" - }, - { - "internalType": "uint256", - "name": "initBalance", - "type": "uint256" - }, - { - "components": [ - { - "internalType": "uint8", - "name": "maxApprovals", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "criteria", - "type": "uint8" - }, - { - "internalType": "address", - "name": "budgetToken", - "type": "address" - }, - { - "internalType": "uint128", - "name": "criteriaValue", - "type": "uint128" - }, - { - "internalType": "uint128", - "name": "budgetAmount", - "type": "uint128" - } - ], - "internalType": "struct ProposalSettings", - "name": "settings", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "descriptionHash", - "type": "bytes32" - } - ], - "name": "propose", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "AlreadyVoted", - "type": "error" - }, - { - "inputs": [], - "name": "BudgetExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "ExistingProposal", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidParams", - "type": "error" - }, - { - "inputs": [], - "name": "MaxApprovalsExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "MaxChoicesExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "NotGovernor", - "type": "error" - }, - { - "inputs": [], - "name": "OptionsNotStrictlyAscending", - "type": "error" - }, - { - "inputs": [], - "name": "WrongProposalId", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json deleted file mode 100644 index f2e29a066bd..00000000000 --- a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json +++ /dev/null @@ -1,258 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_governor", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "COUNTING_MODE", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "PERCENT_DIVISOR", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "PROPOSAL_DATA_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "VOTE_PARAMS_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "uint8", - "name": "", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "name": "_countVote", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "name": "_formatExecuteParams", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "", - "type": "bytes[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" - } - ], - "name": "_voteSucceeded", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "proposals", - "outputs": [ - { - "internalType": "address", - "name": "governor", - "type": "address" - }, - { - "components": [ - { - "internalType": "uint248", - "name": "againstThreshold", - "type": "uint248" - }, - { - "internalType": "bool", - "name": "isRelativeToVotableSupply", - "type": "bool" - } - ], - "internalType": "struct ProposalSettings", - "name": "settings", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "_proposalData", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "_descriptionHash", - "type": "bytes32" - } - ], - "name": "propose", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "AlreadyVoted", - "type": "error" - }, - { - "inputs": [], - "name": "ExistingProposal", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidParams", - "type": "error" - }, - { - "inputs": [], - "name": "NotGovernor", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_NotOptimisticProposalType", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_OptimisticModuleOnlySignal", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_WrongProposalId", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 40ba0366dd0..03a0733070c 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -15,11 +15,6 @@ "internalType": "contract IOptimismGovernor", "name": "_governor", "type": "address" - }, - { - "internalType": "contract IGovernanceToken", - "name": "_votingToken", - "type": "address" } ], "stateMutability": "nonpayable", @@ -77,19 +72,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "VOTING_TOKEN", - "outputs": [ - { - "internalType": "contract IGovernanceToken", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -132,19 +114,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "distributionThreshold", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", @@ -192,7 +161,7 @@ }, { "internalType": "uint256", - "name": "_distributionThreshold", + "name": "_proposalDistributionThreshold", "type": "uint256" }, { @@ -333,6 +302,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proposalDistributionThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "proposalTypesConfigurator", @@ -381,11 +363,11 @@ "inputs": [ { "internalType": "uint256", - "name": "_distributionThreshold", + "name": "_proposalDistributionThreshold", "type": "uint256" } ], - "name": "setDistributionThreshold", + "name": "setProposalDistributionThreshold", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -635,19 +617,6 @@ "stateMutability": "view", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newDistributionThreshold", - "type": "uint256" - } - ], - "name": "DistributionThresholdSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -699,6 +668,19 @@ "name": "ProposalApproved", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newProposalDistributionThreshold", + "type": "uint256" + } + ], + "name": "ProposalDistributionThresholdSet", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index cd05d22a426..babaf48ae2b 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xc4efda2929244bf984fd5a3e32b6a8b5fb68622af6b05a31d3e5f7a25cd6bd3b", - "sourceCodeHash": "0x0064ec36b626190c1d2460aac284df6eaddcb51bf03ff60fa45aecad4ea922c6" + "initCodeHash": "0xe7a93826772bf108a21923f7e45b1f46cdadb75e48b0c796e43d64f0c1d81504", + "sourceCodeHash": "0xadd8e049bf3c652af123b6c64a1d504c92be13b4797ab10b978df907b05dcf7f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json deleted file mode 100644 index 43e2fd35e17..00000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "bytes": "32", - "label": "proposals", - "offset": 0, - "slot": "0", - "type": "mapping(uint256 => struct Proposal)" - }, - { - "bytes": "32", - "label": "accountVotesSet", - "offset": 0, - "slot": "1", - "type": "mapping(uint256 => mapping(address => struct EnumerableSetUpgradeable.UintSet))" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json deleted file mode 100644 index a600d98d300..00000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "bytes": "32", - "label": "proposals", - "offset": 0, - "slot": "0", - "type": "mapping(uint256 => struct Proposal)" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 50c94894f8b..2c01297b2b0 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -43,7 +43,7 @@ }, { "bytes": "32", - "label": "distributionThreshold", + "label": "proposalDistributionThreshold", "offset": 0, "slot": "102", "type": "uint256" diff --git a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol deleted file mode 100644 index 7f189d3f6ec..00000000000 --- a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol +++ /dev/null @@ -1,522 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { EnumerableSetUpgradeable } from - "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; -import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { VotingModule } from "./VotingModule.sol"; - -enum VoteType { - Against, - For, - Abstain -} - -enum PassingCriteria { - Threshold, - TopChoices -} - -struct ExecuteParams { - address targets; - uint256 values; - bytes calldatas; -} - -struct ProposalSettings { - uint8 maxApprovals; - uint8 criteria; - address budgetToken; - uint128 criteriaValue; - uint128 budgetAmount; -} - -struct ProposalOption { - uint256 budgetTokensSpent; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; -} - -struct Proposal { - address governor; - uint256 initBalance; - uint128[] optionVotes; - ProposalOption[] options; - ProposalSettings settings; -} - -contract ApprovalVotingModule is VotingModule { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error WrongProposalId(); - error MaxChoicesExceeded(); - error MaxApprovalsExceeded(); - error BudgetExceeded(); - error OptionsNotStrictlyAscending(); - - /*////////////////////////////////////////////////////////////// - LIBRARIES - //////////////////////////////////////////////////////////////*/ - - using SafeCastLib for uint256; - using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => Proposal) public proposals; - mapping(uint256 => mapping(address => EnumerableSetUpgradeable.UintSet)) private accountVotesSet; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) VotingModule(_governor) { } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * Save settings and options for a new proposal. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. - */ - function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external override { - _onlyGovernor(); - if (proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), proposalData, descriptionHash)))) { - revert WrongProposalId(); - } - - if (proposals[proposalId].governor != address(0)) { - revert ExistingProposal(); - } - - (ProposalOption[] memory proposalOptions, ProposalSettings memory proposalSettings) = - abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - uint256 optionsLength = proposalOptions.length; - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert InvalidParams(); - } - if (proposalSettings.criteria == uint8(PassingCriteria.TopChoices)) { - if (proposalSettings.criteriaValue > optionsLength) { - revert MaxChoicesExceeded(); - } - } - - unchecked { - // Ensure proposal params of each option have the same length between themselves - ProposalOption memory option; - for (uint256 i; i < optionsLength; ++i) { - option = proposalOptions[i]; - if (option.targets.length != option.values.length || option.targets.length != option.calldatas.length) { - revert InvalidParams(); - } - - proposals[proposalId].options.push(option); - } - } - - proposals[proposalId].governor = msg.sender; - proposals[proposalId].settings = proposalSettings; - proposals[proposalId].optionVotes = new uint128[](optionsLength); - } - - /** - * Count approvals voted by `account`. If voting for, options need to be set in ascending order. Votes can only be - * cast once. - * - * @param proposalId The id of the proposal. - * @param account The account to count votes for. - * @param support The type of vote to count. - * @param weight The total vote weight of the `account`. - * @param params The ids of the options to vote for sorted in ascending order, encoded as `uint256[]`. - */ - function _countVote( - uint256 proposalId, - address account, - uint8 support, - uint256 weight, - bytes memory params - ) - external - virtual - override - { - _onlyGovernor(); - Proposal memory proposal = proposals[proposalId]; - - if (support == uint8(VoteType.For)) { - if (weight != 0) { - uint256[] memory options = _decodeVoteParams(params); - uint256 totalOptions = options.length; - if (totalOptions == 0) revert InvalidParams(); - - _recordVote( - proposalId, account, weight.toUint128(), options, totalOptions, proposal.settings.maxApprovals - ); - } - } - } - - /** - * Format executeParams for a governor, given `proposalId` and `proposalData`. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. - * @return targets The targets of the proposal. - * @return values The values of the proposal. - * @return calldatas The calldatas of the proposal. - */ - function _formatExecuteParams( - uint256 proposalId, - bytes memory proposalData - ) - public - override - returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) - { - _onlyGovernor(); - (ProposalOption[] memory options, ProposalSettings memory settings) = - abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - { - IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); - - // If budgetToken is not ETH - if (settings.budgetToken != address(0)) { - // Save initBalance to be used as comparison in `_afterExecute` - proposals[proposalId].initBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); - } - } - - (uint128[] memory sortedOptionVotes, ProposalOption[] memory sortedOptions) = - _sortOptions(proposals[proposalId].optionVotes, options); - - (uint256 executeParamsLength, uint256 succeededOptionsLength) = - _countOptions(sortedOptions, sortedOptionVotes, settings); - - ExecuteParams[] memory executeParams = new ExecuteParams[](executeParamsLength); - executeParamsLength = 0; - uint256 n; - uint256 totalValue; - ProposalOption memory option; - - { - bool budgetExceeded = false; - - // Flatten `options` by filling `executeParams` until budgetAmount is exceeded - for (uint256 i; i < succeededOptionsLength;) { - option = sortedOptions[i]; - - for (n = 0; n < option.targets.length;) { - // If `budgetToken` is ETH and value is not zero, add transaction value to `totalValue` - if (settings.budgetToken == address(0) && option.values[n] != 0) { - if (totalValue + option.values[n] > settings.budgetAmount) { - budgetExceeded = true; - break; // break inner loop - } - totalValue += option.values[n]; - } - - unchecked { - executeParams[executeParamsLength + n] = - ExecuteParams(option.targets[n], option.values[n], option.calldatas[n]); - - ++n; - } - } - - // If `budgetAmount` for ETH is exceeded, skip option. - if (budgetExceeded) break; - - // Check if budgetAmount is exceeded for non-ETH tokens - if (settings.budgetToken != address(0) && settings.budgetAmount != 0) { - if (option.budgetTokensSpent != 0) { - if (totalValue + option.budgetTokensSpent > settings.budgetAmount) break; // break outer loop - // for non-ETH tokens - totalValue += option.budgetTokensSpent; - } - } - - unchecked { - executeParamsLength += n; - - ++i; - } - } - } - - unchecked { - // Increase by one to account for additional `_afterExecute` call - uint256 effectiveParamsLength = executeParamsLength + 1; - - // Init params lengths - targets = new address[](effectiveParamsLength); - values = new uint256[](effectiveParamsLength); - calldatas = new bytes[](effectiveParamsLength); - } - - // Set n `targets`, `values` and `calldatas` - for (uint256 i; i < executeParamsLength;) { - targets[i] = executeParams[i].targets; - values[i] = executeParams[i].values; - calldatas[i] = executeParams[i].calldatas; - - unchecked { - ++i; - } - } - - // Set `_afterExecute` as last call - targets[executeParamsLength] = address(this); - values[executeParamsLength] = 0; - calldatas[executeParamsLength] = - abi.encodeWithSelector(this._afterExecute.selector, proposalId, proposalData, totalValue); - } - - /** - * Hook called by a governor after execute, for `proposalId` with `proposalData`. - * Revert if the transaction has resulted in more tokens being spent than `budgetAmount`. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. - * @param budgetTokensSpent The total amount of tokens that can be spent. - */ - function _afterExecute(uint256 proposalId, bytes memory proposalData, uint256 budgetTokensSpent) public view { - (, ProposalSettings memory settings) = abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - if (settings.budgetToken != address(0) && settings.budgetAmount > 0) { - IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); - - uint256 initBalance = proposals[proposalId].initBalance; - uint256 finalBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); - - // If `finalBalance` is higher than `initBalance`, ignore the budget check - if (finalBalance < initBalance) { - /// @dev Cannot underflow as `finalBalance` is less than `initBalance` - unchecked { - if (initBalance - finalBalance > budgetTokensSpent) { - revert BudgetExceeded(); - } - } - } - } - } - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * Return the ids of the options voted by `account` on `proposalId`. - */ - function getAccountVotes(uint256 proposalId, address account) external view returns (uint256[] memory) { - return accountVotesSet[proposalId][account].values(); - } - - /** - * Return the total number of votes cast by `account` on `proposalId`. - */ - function getAccountTotalVotes(uint256 proposalId, address account) external view returns (uint256) { - return accountVotesSet[proposalId][account].length(); - } - - /** - * @dev Return true if at least one option satisfies the passing criteria. - * Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. - * - * @param proposalId The id of the proposal. - */ - function _voteSucceeded(uint256 proposalId) external view override returns (bool) { - Proposal memory proposal = proposals[proposalId]; - - ProposalOption[] memory options = proposal.options; - uint256 n = options.length; - unchecked { - if (proposal.settings.criteria == uint8(PassingCriteria.Threshold)) { - for (uint256 i; i < n; ++i) { - if (proposal.optionVotes[i] >= proposal.settings.criteriaValue) return true; - } - } else if (proposal.settings.criteria == uint8(PassingCriteria.TopChoices)) { - for (uint256 i; i < n; ++i) { - if (proposal.optionVotes[i] != 0) return true; - } - } - } - - return false; - } - - /** - * Defines the encoding for the expected `proposalData` in `propose`. - * Encoding: `(ProposalOption[], ProposalSettings)` - * - * @dev Can be used by clients to interact with modules programmatically without prior knowledge - * on expected types. - */ - function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { - return - "((uint256 budgetTokensSpent,address[] targets,uint256[] values,bytes[] calldatas,string description)[] proposalOptions,(uint8 maxApprovals,uint8 criteria,address budgetToken,uint128 criteriaValue,uint128 budgetAmount) proposalSettings)"; - } - - /** - * Defines the encoding for the expected `params` in `_countVote`. - * Encoding: `uint256[]` - * - * @dev Can be used by clients to interact with modules programmatically without prior knowledge - * on expected types. - */ - function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { - return "uint256[] optionIds"; - } - - /** - * @dev See {IGovernor-COUNTING_MODE}. - * - * - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - * - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. - * - `params=approvalVote`: params needs to be formatted as `VOTE_PARAMS_ENCODING`. - */ - function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo&quorum=against,for,abstain¶ms=approvalVote"; - } - - /** - * Module version. - */ - function version() public pure returns (uint256) { - return 1; - } - - /*////////////////////////////////////////////////////////////// - INTERNAL - //////////////////////////////////////////////////////////////*/ - - function _recordVote( - uint256 proposalId, - address account, - uint128 weight, - uint256[] memory options, - uint256 totalOptions, - uint256 maxApprovals - ) - internal - { - uint256 option; - uint256 prevOption; - for (uint256 i; i < totalOptions;) { - option = options[i]; - - accountVotesSet[proposalId][account].add(option); - - // Revert if `option` is not strictly ascending - if (i != 0) { - if (option <= prevOption) revert OptionsNotStrictlyAscending(); - } - - prevOption = option; - - /// @dev Revert if `option` is out of bounds - proposals[proposalId].optionVotes[option] += weight; - - unchecked { - ++i; - } - } - - if (accountVotesSet[proposalId][account].length() > maxApprovals) { - revert MaxApprovalsExceeded(); - } - } - - // Sort `options` by `optionVotes` in descending order - function _sortOptions( - uint128[] memory optionVotes, - ProposalOption[] memory options - ) - internal - pure - returns (uint128[] memory, ProposalOption[] memory) - { - unchecked { - uint128 highestValue; - ProposalOption memory highestOption; - uint256 index; - - for (uint256 i; i < optionVotes.length - 1; ++i) { - highestValue = optionVotes[i]; - - for (uint256 j = i + 1; j < optionVotes.length; ++j) { - if (optionVotes[j] > highestValue) { - highestValue = optionVotes[j]; - index = j; - } - } - - if (index != 0) { - optionVotes[index] = optionVotes[i]; - optionVotes[i] = highestValue; - - highestOption = options[index]; - options[index] = options[i]; - options[i] = highestOption; - - index = 0; - } - } - - return (optionVotes, options); - } - } - - // Derive `executeParamsLength` and `succeededOptionsLength` based on passing criteria - function _countOptions( - ProposalOption[] memory options, - uint128[] memory optionVotes, - ProposalSettings memory settings - ) - internal - pure - returns (uint256 executeParamsLength, uint256 succeededOptionsLength) - { - uint256 n = options.length; - unchecked { - uint256 i; - if (settings.criteria == uint8(PassingCriteria.Threshold)) { - // if criteria is `Threshold`, loop through options until `optionVotes` is less than threshold - for (i; i < n; ++i) { - if (optionVotes[i] >= settings.criteriaValue) { - executeParamsLength += options[i].targets.length; - } else { - break; - } - } - } else if (settings.criteria == uint8(PassingCriteria.TopChoices)) { - // if criteria is `TopChoices`, loop through options until the top choices are filled - for (i; i < settings.criteriaValue; ++i) { - if (optionVotes[i] > 0) { - executeParamsLength += options[i].targets.length; - } else { - break; - } - } - } - succeededOptionsLength = i; - } - } - - // Virtual method used to decode _countVote params. - function _decodeVoteParams(bytes memory params) internal virtual returns (uint256[] memory options) { - options = abi.decode(params, (uint256[])); - } -} diff --git a/packages/contracts-bedrock/src/governance/OptimisticModule.sol b/packages/contracts-bedrock/src/governance/OptimisticModule.sol deleted file mode 100644 index 736b73cbb57..00000000000 --- a/packages/contracts-bedrock/src/governance/OptimisticModule.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { IGovernorUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol"; -import { IVotesUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; -import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; -import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { VotingModule } from "./VotingModule.sol"; - -enum VoteType { - Against, - For, - Abstain -} - -struct ProposalSettings { - uint248 againstThreshold; - bool isRelativeToVotableSupply; -} - -struct Proposal { - address governor; - ProposalSettings settings; -} - -contract OptimisticModule is VotingModule { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error OptimisticModule_WrongProposalId(); - error OptimisticModule_NotOptimisticProposalType(); - error OptimisticModule_OptimisticModuleOnlySignal(); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - uint16 public constant PERCENT_DIVISOR = 10_000; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => Proposal) public proposals; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) VotingModule(_governor) { } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Validate proposal is optimistic and save settings for a new proposal. - /// @param _proposalId The id of the proposal. - /// @param _proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. - function propose(uint256 _proposalId, bytes memory _proposalData, bytes32 _descriptionHash) external override { - _onlyGovernor(); - if (_proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), _proposalData, _descriptionHash)))) { - revert OptimisticModule_WrongProposalId(); - } - - if (proposals[_proposalId].governor != address(0)) { - revert ExistingProposal(); - } - - ProposalSettings memory proposalSettings = abi.decode(_proposalData, (ProposalSettings)); - - uint8 proposalTypeId = IOptimismGovernor(msg.sender).getProposalType(_proposalId); - IProposalTypesConfigurator proposalConfigurator = - IProposalTypesConfigurator(IOptimismGovernor(msg.sender).PROPOSAL_TYPES_CONFIGURATOR()); - IProposalTypesConfigurator.ProposalType memory proposalType = proposalConfigurator.proposalTypes(proposalTypeId); - - if (proposalType.quorum != 0 || proposalType.approvalThreshold != 0) { - revert OptimisticModule_NotOptimisticProposalType(); - } - if ( - proposalSettings.againstThreshold == 0 - || (proposalSettings.isRelativeToVotableSupply && proposalSettings.againstThreshold > PERCENT_DIVISOR) - ) { - revert InvalidParams(); - } - - proposals[_proposalId].governor = msg.sender; - proposals[_proposalId].settings = proposalSettings; - } - - /// @notice Counting logic is skipped. - function _countVote(uint256, address, uint8, uint256, bytes memory) external virtual override { } - - /// @notice Reverts to prevent queue and execute of proposals with optimistic module. - function _formatExecuteParams( - uint256, - bytes memory - ) - public - pure - override - returns (address[] memory, uint256[] memory, bytes[] memory) - { - revert OptimisticModule_OptimisticModuleOnlySignal(); - } - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @dev Return true if `againstVotes` is lower than `againstThreshold`. - /// Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. - /// @param _proposalId The id of the proposal. - function _voteSucceeded(uint256 _proposalId) external view override returns (bool) { - Proposal memory proposal = proposals[_proposalId]; - (uint256 againstVotes,,) = IOptimismGovernor(proposal.governor).proposalVotes(_proposalId); - - uint256 againstThreshold = proposal.settings.againstThreshold; - if (proposal.settings.isRelativeToVotableSupply) { - uint256 snapshotBlock = IGovernorUpgradeable(proposal.governor).proposalSnapshot(_proposalId); - IVotesUpgradeable token = IOptimismGovernor(proposal.governor).token(); - againstThreshold = (token.getPastTotalSupply(snapshotBlock) * againstThreshold) / PERCENT_DIVISOR; - } - - return againstVotes < againstThreshold; - } - - /// @dev Defines the encoding for the expected `proposalData` in `propose`. - /// Encoding: `(ProposalSettings)` - /// Can be used by clients to interact with modules programmatically without prior knowledge - /// on expected types. - function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { - return "((uint248 againstThreshold,bool isRelativeToVotableSupply) proposalSettings)"; - } - - /// @dev Defines the encoding for the expected `params` in `_countVote`. - /// Can be used by clients to interact with modules programmatically without prior knowledge - /// on expected types. - function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { - return ""; - } - - /// @dev See {IGovernor-COUNTING_MODE}. - /// - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - /// - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. - function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo&quorum=against,for,abstain"; - } - - /// @notice Module version. - function version() public pure returns (uint256) { - return 1; - } -} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 1aa237ef4be..d9ffaafe21b 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -10,20 +10,12 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; - -// Modules -import { - ProposalSettings as ApprovalProposalSettings, - ProposalOption, - PassingCriteria -} from "src/governance/ApprovalVotingModule.sol"; -import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; -import { VotingModule } from "src/governance/VotingModule.sol"; +import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; +import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; /// @custom:proxied true /// @title ProposalValidator @@ -126,9 +118,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); - /// @notice Emitted when the distribution threshold is set. - /// @param newDistributionThreshold The new distribution threshold. - event DistributionThresholdSet(uint256 newDistributionThreshold); + /// @notice Emitted when the proposal distribution limit is set. + /// @param newProposalDistributionThreshold The new proposal distribution threshold. + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. @@ -225,14 +217,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The governance token contract. - IGovernanceToken public immutable VOTING_TOKEN; - /// @notice The proposal types configurator contract. IProposalTypesConfigurator public proposalTypesConfigurator; /// @notice The max amount of tokens that can be distributed in a proposal. - uint256 public distributionThreshold; + uint256 public proposalDistributionThreshold; /// @notice Mapping of voting cycle numbers to their corresponding data. mapping(uint256 => VotingCycleData) public votingCycles; @@ -244,9 +233,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { mapping(bytes32 => ProposalData) internal _proposals; /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 1.0.0 function version() public pure virtual returns (string memory) { - return "1.0.0-beta.1"; + return "1.0.0"; } /// @notice Constructs the ProposalValidator contract. @@ -254,19 +243,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. - /// @param _votingToken The token used to determine voting power. constructor( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _votingToken + IOptimismGovernor _governor ) ReinitializableBase(1) { APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; GOVERNOR = _governor; - VOTING_TOKEN = _votingToken; _disableInitializers(); } @@ -277,7 +263,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. - /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. + /// @param _proposalDistributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set data for. /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( @@ -287,7 +273,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, - uint256 _distributionThreshold, + uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -300,7 +286,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposalTypesConfigurator = _proposalTypesConfigurator; _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); - _setDistributionThreshold(_distributionThreshold); + _setProposalDistributionThreshold(_proposalDistributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { _setProposalTypeData(_proposalTypes[i], _proposalTypesData[i]); @@ -343,7 +329,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Create OptimisticModule ProposalSettings with required parameters - OptimisticProposalSettings memory optimisticSettings = OptimisticProposalSettings({ + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true // MUST always be true }); @@ -384,7 +370,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) ); emit ProposalMovedToVote(proposalHash_, msg.sender); @@ -424,13 +410,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Build proposal options (elections don't execute operations) - (ProposalOption[] memory options,) = + (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections criteriaValue: _criteriaValue, budgetAmount: 0 // No budget amount for elections @@ -511,13 +497,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Build proposal options with funding execution data - (ProposalOption[] memory options, uint256 totalBudget) = + (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, budgetAmount: uint128(totalBudget) @@ -600,8 +586,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Configure optimistic proposal settings - OptimisticProposalSettings memory settings = - OptimisticProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); + IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); bytes memory proposalVotingModuleData = abi.encode(settings); @@ -641,7 +627,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash @@ -666,13 +652,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Configure approval module options - (ProposalOption[] memory options,) = + (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: _criteriaValue, budgetAmount: 0 @@ -724,7 +710,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash @@ -765,13 +751,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval module options - (ProposalOption[] memory options, uint256 totalBudget) = + (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval module settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, budgetAmount: uint128(totalBudget) @@ -822,9 +808,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingCycles[proposal.votingCycle].movedToVoteTokenCount += totalBudget; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _description, uint8(_proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, uint8(_proposalType)); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -854,9 +839,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Sets the max amount of tokens that can be distributed in a proposal. - /// @param _distributionThreshold The new distribution threshold. - function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { - _setDistributionThreshold(_distributionThreshold); + /// @param _proposalDistributionThreshold The new proposal distribution threshold. + function setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) external onlyOwner { + _setProposalDistributionThreshold(_proposalDistributionThreshold); } /// @notice Sets the data for a proposal type. @@ -960,10 +945,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ) internal view - returns (ProposalOption[] memory options_, uint256 totalBudget_) + returns (IApprovalVotingModule.ProposalOption[] memory options_, uint256 totalBudget_) { uint256 optionsLength = _optionDescriptions.length; - options_ = new ProposalOption[](optionsLength); + options_ = new IApprovalVotingModule.ProposalOption[](optionsLength); for (uint256 i = 0; i < optionsLength; i++) { address[] memory targets; @@ -974,7 +959,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if this is a funding proposal (has recipients and amounts) if (_recipients.length > 0 && _amounts.length > 0) { // Validate amount doesn't exceed distribution threshold - if (_amounts[i] > distributionThreshold) { + if (_amounts[i] > proposalDistributionThreshold) { revert ProposalValidator_ExceedsDistributionThreshold(); } targets = new address[](1); @@ -993,7 +978,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetTokensSpent = 0; } - options_[i] = ProposalOption({ + options_[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: budgetTokensSpent, targets: targets, values: values, @@ -1048,11 +1033,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit VotingCycleDataSet(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } - /// @notice Private function to set the distribution threshold and emit event. - /// @param _distributionThreshold The new distribution threshold. - function _setDistributionThreshold(uint256 _distributionThreshold) private { - distributionThreshold = _distributionThreshold; - emit DistributionThresholdSet(_distributionThreshold); + /// @notice Private function to set the proposal distribution threshold and emit event. + /// @param _proposalDistributionThreshold The new proposal distribution threshold. + function _setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) private { + proposalDistributionThreshold = _proposalDistributionThreshold; + emit ProposalDistributionThresholdSet(_proposalDistributionThreshold); } /// @notice Private function to set a proposal's type data. diff --git a/packages/contracts-bedrock/src/governance/VotingModule.sol b/packages/contracts-bedrock/src/governance/VotingModule.sol deleted file mode 100644 index 02b692d87f1..00000000000 --- a/packages/contracts-bedrock/src/governance/VotingModule.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -abstract contract VotingModule { - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address immutable governor; - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotGovernor(); // nosemgrep: - error ExistingProposal(); // nosemgrep: - error InvalidParams(); // nosemgrep: - error AlreadyVoted(); // nosemgrep: - - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - - function _onlyGovernor() internal view { - if (msg.sender != governor) revert NotGovernor(); - } - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) { - governor = _governor; - } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external virtual; - - function _countVote( - uint256 proposalId, - address account, - uint8 support, - uint256 weight, - bytes memory params - ) - external - virtual; - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - function _formatExecuteParams( - uint256 proposalId, - bytes memory proposalData - ) - external - virtual - returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas); - - function _voteSucceeded(uint256 /* proposalId */ ) external view virtual returns (bool) { - return true; - } - - function COUNTING_MODE() external pure virtual returns (string memory); - - function PROPOSAL_DATA_ENCODING() external pure virtual returns (string memory); - - function VOTE_PARAMS_ENCODING() external pure virtual returns (string memory); -} diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 6971a1de14b..c41765cd037 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.15; // Interfaces import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, @@ -16,6 +15,8 @@ import { import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; +import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; @@ -27,15 +28,6 @@ import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -// Modules -import { - ProposalSettings as ApprovalProposalSettings, - ProposalOption, - PassingCriteria -} from "src/governance/ApprovalVotingModule.sol"; -import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; -import { VotingModule } from "src/governance/VotingModule.sol"; - // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; @@ -46,15 +38,9 @@ contract ProposalValidatorForTest is ProposalValidator { constructor( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _governanceToken + IOptimismGovernor _governor ) - ProposalValidator( - _approvedProposerAttestationSchemaUid, - _topDelegatesAttestationSchemaUid, - _governor, - _governanceToken - ) + ProposalValidator(_approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid, _governor) { } function hashProposalWithModule( @@ -157,7 +143,7 @@ contract ProposalValidator_Init is CommonTest { event VotingCycleDataSet( uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); - event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); @@ -309,7 +295,8 @@ contract ProposalValidator_Init is CommonTest { returns (bytes memory) { // Construct ProposalOption array - ProposalOption[] memory options = new ProposalOption[](descriptions.length); + IApprovalVotingModule.ProposalOption[] memory options = + new IApprovalVotingModule.ProposalOption[](descriptions.length); for (uint256 i = 0; i < descriptions.length; i++) { address[] memory targets = new address[](1); @@ -319,7 +306,7 @@ contract ProposalValidator_Init is CommonTest { targets[0] = Predeploys.GOVERNANCE_TOKEN; calldatas[0] = abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i])); - options[i] = ProposalOption({ + options[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: amounts[i], targets: targets, values: values, @@ -335,9 +322,9 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: criteriaValue, budgetAmount: uint128(totalBudget) @@ -356,14 +343,15 @@ contract ProposalValidator_Init is CommonTest { returns (bytes memory) { // Construct ProposalOption array for elections (no execution calls) - ProposalOption[] memory options = new ProposalOption[](descriptions.length); + IApprovalVotingModule.ProposalOption[] memory options = + new IApprovalVotingModule.ProposalOption[](descriptions.length); for (uint256 i = 0; i < descriptions.length; i++) { address[] memory targets = new address[](0); uint256[] memory values = new uint256[](0); bytes[] memory calldatas = new bytes[](0); - options[i] = ProposalOption({ + options[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: 0, targets: targets, values: values, @@ -373,9 +361,9 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: criteriaValue, budgetAmount: 0 @@ -386,8 +374,8 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper function to construct voting module data for upgrade proposals function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { - OptimisticProposalSettings memory settings = - OptimisticProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); + IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); return abi.encode(settings); } @@ -502,7 +490,7 @@ contract ProposalValidator_Init is CommonTest { proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor ); validator = ProposalValidatorForTest(address(new Proxy(owner))); @@ -600,7 +588,7 @@ contract ProposalValidator_Init is CommonTest { contract ProposalValidator_Version_Test is ProposalValidator_Init { function test_version_succeeds() public view { string memory versionString = validator.version(); - assertEq(versionString, "1.0.0-beta.1"); + assertEq(versionString, "1.0.0"); } } @@ -613,7 +601,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor ); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -644,7 +632,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); // Verify initialization was successful - assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.proposalDistributionThreshold(), DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); // Verify voting cycle data @@ -768,7 +756,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -981,7 +969,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -1569,7 +1557,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); } - function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts( + function testFuzz_submitFundingProposal_exceedsProposalDistributionThreshold_reverts( uint256 excessAmount, uint8 proposalTypeValue ) @@ -1967,7 +1955,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -2060,7 +2048,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2099,7 +2087,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -2206,7 +2194,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2278,7 +2266,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I abi.encodeCall( IOptimismGovernor.proposeWithModule, ( - VotingModule(approvalVotingModule), + approvalVotingModule, governanceFundVotingModuleData, governanceFundProposalDescription, uint8(governanceFundProposalType) @@ -2318,7 +2306,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I abi.encodeCall( IOptimismGovernor.proposeWithModule, ( - VotingModule(approvalVotingModule), + approvalVotingModule, councilBudgetVotingModuleData, councilBudgetProposalDescription, uint8(councilBudgetProposalType) @@ -2551,7 +2539,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } - function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsProposalDistributionThreshold_reverts( uint8 _proposalTypeValue ) public @@ -2656,7 +2644,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2731,23 +2719,23 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { validator.setVotingCycleData(CYCLE_NUMBER, startingTimestamp, duration, distributionLimit); } - function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { - // Expect the DistributionThresholdSet event to be emitted + function testFuzz_setProposalDistributionThreshold_succeeds(uint256 newProposalDistributionThreshold) public { + // Expect the ProposalDistributionThresholdSet event to be emitted vm.expectEmit(address(validator)); - emit DistributionThresholdSet(newDistributionThreshold); + emit ProposalDistributionThresholdSet(newProposalDistributionThreshold); vm.prank(owner); - validator.setDistributionThreshold(newDistributionThreshold); + validator.setProposalDistributionThreshold(newProposalDistributionThreshold); - assertEq(validator.distributionThreshold(), newDistributionThreshold); + assertEq(validator.proposalDistributionThreshold(), newProposalDistributionThreshold); } - function testFuzz_setDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { + function testFuzz_setProposalDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { vm.assume(caller != owner); vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setDistributionThreshold(threshold); + validator.setProposalDistributionThreshold(threshold); } function testFuzz_setProposalTypeData_succeeds( From 142d12947cc707844201c3af18b1842ebcde2c24 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 18 Jul 2025 05:57:03 -0300 Subject: [PATCH 21/73] fix: check hash after proposal (#447) * fix: add proposal id validation on submit upgrade proposal returned proposalId * fix: pre-pr --- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 7 +++- .../test/governance/ProposalValidator.t.sol | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index babaf48ae2b..f9436c64be4 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xe7a93826772bf108a21923f7e45b1f46cdadb75e48b0c796e43d64f0c1d81504", - "sourceCodeHash": "0xadd8e049bf3c652af123b6c64a1d504c92be13b4797ab10b978df907b05dcf7f" + "initCodeHash": "0x06a2b713907a4a4c961061edeb8f9417f9fdf63fc5512e6794af67fcc548935d", + "sourceCodeHash": "0x98cb001a29058d9d4bdd017c73e3520af1e22e9dee15e153799196b3390923df" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index d9ffaafe21b..f37a40d1a7f 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -369,10 +369,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { if (_proposalType == ProposalType.MaintenanceUpgrade) { proposal.movedToVote = true; - GOVERNOR.proposeWithModule( + uint256 proposalId = GOVERNOR.proposeWithModule( votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) ); + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + emit ProposalMovedToVote(proposalHash_, msg.sender); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index c41765cd037..b538055e2c2 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1077,6 +1077,44 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); } + + function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 proposalId) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + vm.assume(proposalId != uint256(expectedHash)); // Ensure proposalId is different from expectedHash + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Mock the proposeWithModule call to return a different proposalId + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(proposalId) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test From d18f80b411f43fba605e3573e9699d853131c7eb Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:10:32 -0300 Subject: [PATCH 22/73] refactor: fetch proposal types configurator externally (#448) * refactor: fetch configurator from governor * fix: pre-pr * fix: pre-pr --- .../governance/IProposalValidator.sol | 4 --- .../snapshots/abi/ProposalValidator.json | 18 ----------- .../snapshots/semver-lock.json | 4 +-- .../storageLayout/ProposalValidator.json | 15 +++------- .../src/governance/ProposalValidator.sol | 30 +++++++++---------- .../test/governance/ProposalValidator.t.sol | 9 ++++-- 6 files changed, 26 insertions(+), 54 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 5c0c48a7be4..bf2c2e43778 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.0; // Interfaces import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. @@ -167,7 +166,6 @@ interface IProposalValidator is ISemver { function initialize( address _owner, - IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, uint256 _startingTimestamp, uint256 _duration, @@ -195,8 +193,6 @@ interface IProposalValidator is ISemver { function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 03a0733070c..9eddf4e0965 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -134,11 +134,6 @@ "name": "_owner", "type": "address" }, - { - "internalType": "contract IProposalTypesConfigurator", - "name": "_proposalTypesConfigurator", - "type": "address" - }, { "internalType": "uint256", "name": "_cycleNumber", @@ -315,19 +310,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "proposalTypesConfigurator", - "outputs": [ - { - "internalType": "contract IProposalTypesConfigurator", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f9436c64be4..7934d0b6b48 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x06a2b713907a4a4c961061edeb8f9417f9fdf63fc5512e6794af67fcc548935d", - "sourceCodeHash": "0x98cb001a29058d9d4bdd017c73e3520af1e22e9dee15e153799196b3390923df" + "initCodeHash": "0xb612561f3182537796ec6bae6a15a4f6b9e63d839e051a165f6b81b3f6ce0807", + "sourceCodeHash": "0xbfb241a7033264d5b7097a4326a415b53514c4a4dee01430bd092fa6cbf0872b" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 2c01297b2b0..e8bbb446de6 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,39 +34,32 @@ "slot": "52", "type": "uint256[49]" }, - { - "bytes": "20", - "label": "proposalTypesConfigurator", - "offset": 0, - "slot": "101", - "type": "contract IProposalTypesConfigurator" - }, { "bytes": "32", "label": "proposalDistributionThreshold", "offset": 0, - "slot": "102", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "103", + "slot": "102", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "105", + "slot": "104", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f37a40d1a7f..cf839cfe8b0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -217,9 +217,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The proposal types configurator contract. - IProposalTypesConfigurator public proposalTypesConfigurator; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -258,7 +255,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. - /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _cycleNumber The number of the current voting cycle. /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. @@ -268,7 +264,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, - IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, uint256 _startingTimestamp, uint256 _duration, @@ -284,7 +279,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - proposalTypesConfigurator = _proposalTypesConfigurator; _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -338,8 +332,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Get the optimistic module address from configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = @@ -430,7 +425,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = proposalTypesConfigurator.proposalTypes( + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule ).module; @@ -517,8 +512,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); @@ -598,7 +594,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; - address votingModule = proposalTypesConfigurator.proposalTypes( + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule ).module; @@ -673,8 +669,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator ProposalType proposalType = ProposalType.CouncilMemberElections; - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = @@ -771,8 +768,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b538055e2c2..bf90d08d5c6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -464,6 +464,12 @@ contract ProposalValidator_Init is CommonTest { moduleAddress = optimisticVotingModule; } + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.PROPOSAL_TYPES_CONFIGURATOR, ()), + abi.encode(proposalTypesConfigurator) + ); + _mockAndExpect( address(proposalTypesConfigurator), abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), @@ -501,7 +507,6 @@ contract ProposalValidator_Init is CommonTest { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, @@ -619,7 +624,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, @@ -691,7 +695,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, From a32244fd427bad17ae5e931e222ab12dd50f2251 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 21 Jul 2025 06:08:59 -0300 Subject: [PATCH 23/73] fix: check for uninitialized voting modules (#446) * fix: check for uninitialized voting modules * fix: pre-pr * fix: pre-pr --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 39 +++++++--- .../test/governance/ProposalValidator.t.sol | 74 +++++++++++++++++++ 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index bf2c2e43778..b932d6e2461 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -29,6 +29,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_ProposalIdMismatch(); error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); + error ProposalValidator_InvalidVotingModule(); event ProposalSubmitted( bytes32 indexed proposalHash, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9eddf4e0965..7a1bb786918 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -853,6 +853,11 @@ "name": "ProposalValidator_InvalidVotingCycle", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidVotingModule", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 7934d0b6b48..e4ab017c9c0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xb612561f3182537796ec6bae6a15a4f6b9e63d839e051a165f6b81b3f6ce0807", - "sourceCodeHash": "0xbfb241a7033264d5b7097a4326a415b53514c4a4dee01430bd092fa6cbf0872b" + "initCodeHash": "0xd072e288107128a1b0f26f41c4b257295919777cadfc06514d2a4738fb96e1d1", + "sourceCodeHash": "0x725efcbb68cb4a23af492983ba22969ddaa5d472878fb8490a437eaaa88812a8" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index cf839cfe8b0..f8f320e46bc 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -86,6 +86,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the proposal is invalid trying to move to vote. error ProposalValidator_InvalidProposal(); + /// @notice Thrown when the voting module address is invalid (zero address). + error ProposalValidator_InvalidVotingModule(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -332,9 +335,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Get the optimistic module address from configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = @@ -425,9 +434,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = @@ -512,9 +527,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index bf90d08d5c6..4bf2204b063 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -485,6 +485,36 @@ contract ProposalValidator_Init is CommonTest { ); } + /// @notice Helper function to mock proposal types configurator call with changed module + function _mockProposalTypesConfiguratorCallWithUninitializedModule(uint8 _votingModuleId) internal { + address moduleAddress; + if (_votingModuleId == APPROVAL_VOTING_MODULE_ID) { + moduleAddress = approvalVotingModule; + } else if (_votingModuleId == OPTIMISTIC_VOTING_MODULE_ID) { + moduleAddress = optimisticVotingModule; + } + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.PROPOSAL_TYPES_CONFIGURATOR, ()), + abi.encode(proposalTypesConfigurator) + ); + + _mockAndExpect( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 0, + approvalThreshold: 0, + name: "", + description: "", + module: address(0) + }) + ) + ); + } + /// @notice Initializes the validator function _initializeValidator() internal virtual { ( @@ -943,6 +973,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function test_submitUpgradeProposal_invalidVotingModule_reverts() public { + uint248 againstThreshold = 5000; // 50% + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); @@ -1377,6 +1422,20 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER ); } + + function test_submitCouncilMemberElectionsProposal_invalidVotingModule_reverts() public { + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_SubmitFundingProposal_Test @@ -1742,6 +1801,21 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I CYCLE_NUMBER ); } + + function test_submitFundingProposal_invalidVotingModule_reverts() public { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.GovernanceFund; + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_ApproveProposal_Test From 74a0363b9e2979505dba43f6c2482789618acd24 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:11:29 +0300 Subject: [PATCH 24/73] feat: add check in approve if proposal has moved to vote (#450) --- .../snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 5 +++++ .../test/governance/ProposalValidator.t.sol | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index e4ab017c9c0..4fc8cd41838 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd072e288107128a1b0f26f41c4b257295919777cadfc06514d2a4738fb96e1d1", - "sourceCodeHash": "0x725efcbb68cb4a23af492983ba22969ddaa5d472878fb8490a437eaaa88812a8" + "initCodeHash": "0x30d570ce61624852476452d9660567bae2346002878a45703cf60e44809730ca", + "sourceCodeHash": "0xe5272e8176b0ddf3ff589629f7872b5dc6c18b900f413f0f8682865bf310faa7" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f8f320e46bc..6d2dc68fb9b 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -578,6 +578,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyApproved(); } + // check if proposal has already moved to vote + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + // validate the attestation _validateTopDelegateAttestation(_attestationUid, _msgSender()); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 4bf2204b063..92f8863dfb5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1881,6 +1881,23 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalHash, topDelegateAttestation_A); } + function test_approveProposal_proposalAlreadyMovedToVote_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists and set movedToVote to true + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); From 154065c9ab24c721e2e65401ac7240cf64153834 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:17:29 +0300 Subject: [PATCH 25/73] fix: msg sender to be consistent (#451) --- .../contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 4fc8cd41838..754d18cb890 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x30d570ce61624852476452d9660567bae2346002878a45703cf60e44809730ca", - "sourceCodeHash": "0xe5272e8176b0ddf3ff589629f7872b5dc6c18b900f413f0f8682865bf310faa7" + "initCodeHash": "0x0dfc44cf456909f524602c8d2b3e5793e04b59da994a8268bc59071aad567c77", + "sourceCodeHash": "0xbe31385272e8ecf4fd0bf52e768efcf9b6b4199f23707d74949c11ee6578e6de" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 6d2dc68fb9b..3a4659445f0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -362,11 +362,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); + emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) @@ -382,7 +382,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, msg.sender); + emit ProposalMovedToVote(proposalHash_, _msgSender()); } } @@ -461,11 +461,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = ProposalType.CouncilMemberElections; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } @@ -553,11 +553,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); + emit ProposalSubmitted(proposalHash_, _msgSender(), _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } @@ -915,7 +915,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { if ( attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID - || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) + || approvedDelegate != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } From 261fc72cf9f9ec8a37d68d2491e494acb7972896 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:35:37 +0300 Subject: [PATCH 26/73] fix: approved proposer schema (#452) --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 14 ++++-- .../test/governance/ProposalValidator.t.sol | 46 +++++++++++++++---- 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index b932d6e2461..7d93349984e 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -21,6 +21,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); error ProposalValidator_AttestationRevoked(); + error ProposalValidator_AttestationExpired(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); error ProposalValidator_InvalidAgainstThreshold(); diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 7a1bb786918..2afaef89b24 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -788,6 +788,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationExpired", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_AttestationRevoked", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 754d18cb890..fd811a1f8d4 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x0dfc44cf456909f524602c8d2b3e5793e04b59da994a8268bc59071aad567c77", - "sourceCodeHash": "0xbe31385272e8ecf4fd0bf52e768efcf9b6b4199f23707d74949c11ee6578e6de" + "initCodeHash": "0xbac284f6ec21a5d65d5b86d7e6406e0805d77e15dc4bd66397f0111701110e0e", + "sourceCodeHash": "0x58048692d1da18d17958b2dc950055ae2a58ab645385b0aed3528b289dfee21d" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3a4659445f0..c871a2b9431 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -62,6 +62,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an attestation is revoked. error ProposalValidator_AttestationRevoked(); + /// @notice Thrown when the attestation is expired. + error ProposalValidator_AttestationExpired(); + /// @notice Thrown when an attestation schema is invalid. error ProposalValidator_InvalidAttestationSchema(); @@ -210,7 +213,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. - /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } + /// @dev Schema format: { proposalType: uint8, date: string } bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller @@ -911,11 +914,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } - (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); + // check if the attestation is expired + if (attestation.expirationTime != 0 && attestation.expirationTime < block.timestamp) { + revert ProposalValidator_AttestationExpired(); + } + + (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID - || approvedDelegate != _msgSender() || proposalType != uint8(_expectedProposalType) + || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 92f8863dfb5..edab6a51f1a 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -115,6 +115,7 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; + uint64 public constant ATT_EXPIRATION_TIME = 10 days; address owner; address user; @@ -561,7 +562,7 @@ contract ProposalValidator_Init is CommonTest { // Create schemas vm.prank(owner); APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), true + "uint8 proposalType,string date", ISchemaResolver(address(0)), true ); vm.prank(owner); @@ -588,11 +589,11 @@ contract ProposalValidator_Init is CommonTest { AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: _delegate, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: true, refUID: bytes32(0), - data: abi.encode(_delegate, _proposalType), + data: abi.encode(_proposalType, "2000-01-01"), value: 0 }) }) @@ -948,6 +949,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 proposalTypeValue) public { + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // warp the time to after the attestation expiration time + vm.warp(block.timestamp + ATT_EXPIRATION_TIME + 1); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { uint248 zeroThreshold = 0; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -1083,11 +1099,11 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: topDelegate_A, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: false, refUID: bytes32(0), - data: abi.encode(topDelegate_A, proposalType), + data: abi.encode(proposalType, "2000-01-01"), value: 0 }) }) @@ -1292,6 +1308,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } + function testFuzz_submitCouncilMemberElectionsProposal_attestationExpired_reverts() public { + // warp the time to after the attestation expiration time + vm.warp(block.timestamp + ATT_EXPIRATION_TIME + 1); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { string[] memory emptyOptions = new string[](0); @@ -1370,11 +1396,11 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: topDelegate_A, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: false, refUID: bytes32(0), - data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + data: abi.encode(ProposalValidator.ProposalType.CouncilMemberElections, "2000-01-01"), value: 0 }) }) From 6a4a6fcb7005f31ef260371b6592bddff9d66cf5 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:04:42 +0300 Subject: [PATCH 27/73] fix: voting cycle validity on submit (#454) * fix: voting cycle validity on submit * fix: edge case on upgrade proposals --- .../governance/IProposalValidator.sol | 2 +- .../snapshots/abi/ProposalValidator.json | 2 +- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 25 +++++++- .../test/governance/ProposalValidator.t.sol | 59 +++++++++++++++---- 5 files changed, 74 insertions(+), 18 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7d93349984e..f270847582d 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -107,7 +107,7 @@ interface IProposalValidator is ISemver { string memory _proposalDescription, bytes32 _attestationUid, ProposalType _proposalType, - uint256 _votingCycle + uint256 _latestVotingCycle ) external returns (bytes32 proposalHash_); function submitCouncilMemberElectionsProposal( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 2afaef89b24..044e2188e95 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -524,7 +524,7 @@ }, { "internalType": "uint256", - "name": "_votingCycle", + "name": "_latestVotingCycle", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index fd811a1f8d4..fe9a1982193 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xbac284f6ec21a5d65d5b86d7e6406e0805d77e15dc4bd66397f0111701110e0e", - "sourceCodeHash": "0x58048692d1da18d17958b2dc950055ae2a58ab645385b0aed3528b289dfee21d" + "initCodeHash": "0x33e740162106c353e5d9f550c67d47286846b80a91f042f5b600703e4ed0b42c", + "sourceCodeHash": "0xb51010203b3a894896a1e70c999f858d6d8e937c08feb301642e93f9787081e5" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c871a2b9431..704bfd2653f 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -302,14 +302,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). - /// @param _votingCycle The voting cycle number the proposal is targetted for. + /// @param _latestVotingCycle The latest voting cycle number. Even though the upgrade proposal can be submitted + /// outside of a voting cycle, we still need the latest voting cycle number to validate top delegates attestations. /// @return proposalHash_ The hash of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, bytes32 _attestationUid, ProposalType _proposalType, - uint256 _votingCycle + uint256 _latestVotingCycle ) external returns (bytes32 proposalHash_) @@ -320,6 +321,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidUpgradeProposalType(); } + // Validate voting cycle exists + VotingCycleData memory latestVotingCycleData = votingCycles[_latestVotingCycle]; + if (latestVotingCycleData.startingTimestamp == 0) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate EAS attestation - must be called by owner-approved address _validateApprovedProposerAttestation(_attestationUid, _proposalType); @@ -367,7 +374,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; - proposal.votingCycle = _votingCycle; + proposal.votingCycle = _latestVotingCycle; emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -407,6 +414,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (bytes32 proposalHash_) { + // Validate voting cycle exists and is not in the past + VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; + if (votingCycleData.startingTimestamp == 0 || votingCycleData.startingTimestamp < block.timestamp) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate EAS attestation - must be called by owner-approved address _validateApprovedProposerAttestation(_attestationUid, ProposalType.CouncilMemberElections); @@ -503,6 +516,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidFundingProposalType(); } + // Validate voting cycle exists and is not in the past + VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; + if (votingCycleData.startingTimestamp == 0 || votingCycleData.startingTimestamp < block.timestamp) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate input arrays have matching lengths uint256 optionsLength = _optionsDescriptions.length; if (optionsLength != _optionsRecipients.length || optionsLength != _optionsAmounts.length) { diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index edab6a51f1a..b70fb6e43a7 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -895,6 +895,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init /// @notice Sad path tests for submitUpgradeProposal function contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { string proposalDescription; + uint248 againstThreshold = 5000; // 50% function setUp() public override { super.setUp(); @@ -910,7 +911,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; // 50% bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); @@ -920,8 +920,25 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitUpgradeProposal_invalidVotingCycle_reverts( + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + vm.assume(votingCycle != CYCLE_NUMBER); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, votingCycle + ); + } + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -937,7 +954,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -952,7 +968,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 proposalTypeValue) public { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // warp the time to after the attestation expiration time @@ -990,7 +1005,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I } function test_submitUpgradeProposal_invalidVotingModule_reverts() public { - uint248 againstThreshold = 5000; // 50% ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1009,7 +1023,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal hash @@ -1061,7 +1074,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal hash @@ -1090,7 +1102,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; // Create attestation but don't use proper owner as attester @@ -1121,8 +1132,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; - // Create valid attestation first (make it revocable) bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1143,7 +1152,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I } function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 proposalId) public { - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1285,6 +1293,15 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } + function testFuzz_submitCouncilMemberElectionsProposal_invalidVotingCycle_reverts(uint256 votingCycle) public { + vm.assume(votingCycle != CYCLE_NUMBER); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, votingCycle + ); + } + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { @@ -1581,6 +1598,26 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitFundingProposal_invalidVotingCycle_reverts( + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + vm.assume(votingCycle != CYCLE_NUMBER); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, votingCycle + ); + } + function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( uint8 matchingLength, uint8 mismatchedLength, From 663858467ef936bb65de4c955be436660a275ebf Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 22 Jul 2025 22:14:08 +0300 Subject: [PATCH 28/73] fix: budget cap dos (#453) * fix: budget cap dos * fix: invalid proposal case * fix: test * fix: tests --- .../governance/IProposalValidator.sol | 3 +- .../snapshots/abi/ProposalValidator.json | 10 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 52 +++++++++++-- .../test/governance/ProposalValidator.t.sol | 76 +++++++++++++++++-- 5 files changed, 132 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index f270847582d..2682c9eb5a7 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -31,6 +31,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); error ProposalValidator_InvalidVotingModule(); + error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( bytes32 indexed proposalHash, @@ -130,7 +131,7 @@ interface IProposalValidator is ISemver { function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + function canApproveProposal(bytes32 _attestationUid, address _delegate, bytes32 _proposalHash) external view returns (bool canApprove_); function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 044e2188e95..9c41904e7fd 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -101,6 +101,11 @@ "internalType": "address", "name": "_delegate", "type": "address" + }, + { + "internalType": "bytes32", + "name": "_proposalHash", + "type": "bytes32" } ], "name": "canApproveProposal", @@ -788,6 +793,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationCreatedAfterLastVotingCycle", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_AttestationExpired", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index fe9a1982193..cef9eab6c85 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x33e740162106c353e5d9f550c67d47286846b80a91f042f5b600703e4ed0b42c", - "sourceCodeHash": "0xb51010203b3a894896a1e70c999f858d6d8e937c08feb301642e93f9787081e5" + "initCodeHash": "0xd359b54afbf8f0bfcde0e24b648d20f1eecdc306b511d3c0cd90afa5b61382ac", + "sourceCodeHash": "0x6da6042e1bc89da33dad2ce39aaef685f2a67c04e4693f69c2693b349899d131" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 704bfd2653f..537d808e314 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -92,6 +92,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the voting module address is invalid (zero address). error ProposalValidator_InvalidVotingModule(); + /// @notice Thrown when the attestation was created after the last voting cycle. + error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -591,7 +594,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address _delegate = _msgSender(); ProposalData storage proposal = _proposals[_proposalHash]; // check if the proposal exists - if (proposal.proposer == address(0)) { + // proposal.votingCycle should never be 0, voting cycles already exist before the ProposalValidator is deployed + // and should be set by the OP Foundation + if (proposal.proposer == address(0) || proposal.votingCycle == 0) { revert ProposalValidator_ProposalDoesNotExist(); } @@ -605,8 +610,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyMovedToVote(); } + // The previous voting cycle of a proposal should be the one before the + // proposal's targetted voting cycle. + uint256 previousVotingCycle = proposal.votingCycle - 1; + // Proposal or Governor Upgrade proposals are submitted with the latest voting cycle number, + // because they can be submitted outside of a voting cycle. + if (proposal.proposalType == ProposalType.ProtocolOrGovernorUpgrade) { + previousVotingCycle = proposal.votingCycle; + } + // validate the attestation - _validateTopDelegateAttestation(_attestationUid, _msgSender()); + _validateTopDelegateAttestation(_attestationUid, _msgSender(), previousVotingCycle); // store the approval proposal.delegateApprovals[_delegate] = true; @@ -618,9 +632,25 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Checks if a delegate can approve a proposal. /// @dev Helper function for UI integration. /// @param _attestationUid The UID of the attestation to check. + /// @param _delegate The delegate to check the attestation for. + /// @param _proposalHash The hash of the proposal to check the attestation for. /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + function canApproveProposal( + bytes32 _attestationUid, + address _delegate, + bytes32 _proposalHash + ) + external + view + returns (bool canApprove_) + { + // TODO: this function should be fixed in OPT-957 + ProposalData storage proposal = _proposals[_proposalHash]; + if (proposal.votingCycle == 0) { + return false; + } + + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate, proposal.votingCycle - 1); } /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. @@ -955,13 +985,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @return canApprove_ True if the attestation is valid, false otherwise. function _validateTopDelegateAttestation( bytes32 _attestationUid, - address _delegate + address _delegate, + uint256 _lastVotingCycle ) internal view returns (bool canApprove_) { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + VotingCycleData memory previousVotingCycleData = votingCycles[_lastVotingCycle]; + if (previousVotingCycleData.startingTimestamp == 0) { + revert ProposalValidator_InvalidVotingCycle(); + } // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) if (attestation.uid == bytes32(0)) { @@ -978,6 +1013,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } + // since the attestations are updated daily we should only allow attestations + // created before the last voting cycle of the proposal + // check if attestation was created after the previous voting cycle + if (attestation.time > previousVotingCycleData.startingTimestamp + previousVotingCycleData.duration) { + revert ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + } + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); // check if the attestation includes partial delegation or the recipient is not the caller diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b70fb6e43a7..7e88af159b6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1884,6 +1884,15 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + function setUp() public override { + super.setUp(); + + // create a new voting cycle + // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + } + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Ensure the proposal hash is not 0 vm.assume(_proposalHash != bytes32(0)); @@ -1961,6 +1970,25 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalHash, topDelegateAttestation_A); } + function test_approveProposal_invalidVotingCycle_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + vm.assume(votingCycle != CYCLE_NUMBER); + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, votingCycle); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); @@ -1990,6 +2018,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // set proposal data so that the proposal exists validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); @@ -2002,6 +2033,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // revoke the attestation vm.prank(owner); @@ -2033,6 +2067,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -2052,6 +2089,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // create an attestation with partial delegation vm.prank(owner); @@ -2091,6 +2131,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -2102,20 +2145,34 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public view { + function test_canApproveProposal_returnTrue_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set the voting cycle data of the previous cycle + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); assertTrue(canApprove); } - function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { + function test_canApproveProposal_returnFalseRevert_succeeds( + bytes32 _attestationUid, + address _delegate, + bytes32 _proposalHash + ) + public + { // Ensure the attestation uid is not one of the top delegates - vm.assume(attestationUid != topDelegateAttestation_A); + vm.assume(_attestationUid != topDelegateAttestation_A); bool canApprove; // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { + try validator.canApproveProposal(_attestationUid, _delegate, _proposalHash) returns (bool result_) { canApprove = result_; } catch { canApprove = false; @@ -2123,6 +2180,15 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { assertEq(canApprove, false); } + + function test_canApproveProposal_returnFalseProposalNotFound_reverts(bytes32 _proposalHash) public { + validator.setProposalData( + _proposalHash, topDelegate_A, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, 0, 0 + ); + + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); + assertEq(canApprove, false); + } } /// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test From 505ae505cc8c30d4a4003618827963d26fbef1a8 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:33:05 -0300 Subject: [PATCH 29/73] fix: move to vote logic (#455) * refactor: improve variable naming * fix: wrong arg sent to external proposeWithModuole calls * fix: pre-pr * fix: stack too deep error * fix: pre-pr * fix: pre-pr --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 6 +- .../snapshots/abi/ProposalValidator.json | 8 +- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 100 ++++++++++-------- .../test/governance/ProposalValidator.t.sol | 76 ++++++------- 5 files changed, 97 insertions(+), 97 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 2682c9eb5a7..090499667d3 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -62,7 +62,7 @@ interface IProposalValidator is ISemver { event ProposalTypeDataSet( ProposalType proposalType, uint256 requiredApprovals, - uint8 proposalVotingModule + uint8 idInConfigurator ); event ProposalVotingModuleData( @@ -85,7 +85,7 @@ interface IProposalValidator is ISemver { struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalVotingModule; + uint8 idInConfigurator; } struct VotingCycleData { @@ -196,7 +196,7 @@ interface IProposalValidator is ISemver { function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 idInConfigurator); function votingCycles(uint256) external view returns ( uint256 startingTimestamp, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9c41904e7fd..66b2363eda4 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -178,7 +178,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -332,7 +332,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -375,7 +375,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -736,7 +736,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index cef9eab6c85..ae6cdf304eb 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd359b54afbf8f0bfcde0e24b648d20f1eecdc306b511d3c0cd90afa5b61382ac", - "sourceCodeHash": "0x6da6042e1bc89da33dad2ce39aaef685f2a67c04e4693f69c2693b349899d131" + "initCodeHash": "0xca95fa18ecb8d7d44043dd60c545719e526fdf487ea405c8321d25b92782bfd6", + "sourceCodeHash": "0xf0b71a440952d0a4a00a488f5b98da2010d1972297876ea8ade494d0885f7508" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 537d808e314..42242128349 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -134,8 +134,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal type ID. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + /// @param idInConfigurator The proposal type ID. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. /// @param proposalHash The hash of the submitted proposal. @@ -165,10 +165,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing explicit data for each proposal type. /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. - /// @param proposalVotingModule The proposal type ID used to get the voting module from the configurator. + /// @param idInConfigurator The proposal type ID used to get the voting module from the configurator. struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalVotingModule; + uint8 idInConfigurator; } /// @notice Struct for storing voting cycle data. @@ -325,8 +325,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Validate voting cycle exists - VotingCycleData memory latestVotingCycleData = votingCycles[_latestVotingCycle]; - if (latestVotingCycleData.startingTimestamp == 0) { + if (votingCycles[_latestVotingCycle].startingTimestamp == 0) { revert ProposalValidator_InvalidVotingCycle(); } @@ -338,24 +337,29 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidAgainstThreshold(); } - // Create OptimisticModule ProposalSettings with required parameters - IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ - againstThreshold: _againstThreshold, - isRelativeToVotableSupply: true // MUST always be true - }); - // Optimistic proposals are signal-only, no execution targets/calldatas needed - bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); + bytes memory proposalVotingModuleData = abi.encode( + IOptimisticModule.ProposalSettings({ + againstThreshold: _againstThreshold, + isRelativeToVotableSupply: true // MUST always be true + }) + ); + + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; // Get the optimistic module address from configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); - address votingModule = proposalTypeConfig.module; + address votingModule; + { + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); - // Validate voting module exists - if (bytes(proposalTypeConfig.name).length == 0) { - revert ProposalValidator_InvalidVotingModule(); + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } + + votingModule = proposalTypeConfig.module; } // Generate unique proposal hash @@ -387,7 +391,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator ); // Make sure the proposalId is the same as the proposalHash @@ -455,7 +459,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule); + ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator); address votingModule = proposalTypeConfig.module; // Validate voting module exists @@ -554,7 +558,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + ).proposalTypes(proposalTypesData[_proposalType].idInConfigurator); address votingModule = proposalTypeConfig.module; // Validate voting module exists @@ -670,11 +674,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(settings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].idInConfigurator; + // Get the module address from the configurator ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = @@ -705,9 +711,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -745,11 +750,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); + ProposalType _proposalType = ProposalType.CouncilMemberElections; + + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - ProposalType proposalType = ProposalType.CouncilMemberElections; - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[proposalType].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = @@ -758,7 +766,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ProposalData storage proposal = _proposals[proposalHash_]; // Proposal must exist and be valid - if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { revert ProposalValidator_InvalidProposal(); } @@ -768,7 +776,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Check if proposal has enough approvals - if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { + if (proposal.approvalCount < proposalTypesData[_proposalType].requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } @@ -789,9 +797,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -824,7 +831,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (bytes32 proposalHash_) { - uint256 optionsLength = _optionsDescriptions.length; // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { revert ProposalValidator_InvalidFundingProposalType(); @@ -836,7 +842,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Configure approval module settings IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(optionsLength), + maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, @@ -845,10 +851,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); @@ -890,7 +898,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = - GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, uint8(_proposalType)); + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -1143,8 +1151,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypeData The data for the proposal type. function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { proposalTypesData[_proposalType] = _proposalTypeData; - emit ProposalTypeDataSet( - _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalVotingModule - ); + emit ProposalTypeDataSet(_proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.idInConfigurator); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7e88af159b6..15eea020bd5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -146,7 +146,7 @@ contract ProposalValidator_Init is CommonTest { ); event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( - ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator ); event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); @@ -167,9 +167,9 @@ contract ProposalValidator_Init is CommonTest { stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(0) .checked_write(_data.requiredApprovals); - // Set proposalVotingModule (depth 1) + // Set idInConfigurator (depth 1) stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(1) - .checked_write(_data.proposalVotingModule); + .checked_write(_data.idInConfigurator); } /// @notice Helper function to set CouncilMemberElections proposal type data. @@ -178,7 +178,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilMemberElections, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -189,7 +189,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -200,7 +200,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -211,7 +211,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }) ); } @@ -222,7 +222,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.MaintenanceUpgrade, ProposalValidator.ProposalTypeData({ requiredApprovals: 0, // MaintenanceUpgrade moves directly to voting - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }) ); } @@ -259,27 +259,25 @@ contract ProposalValidator_Init is CommonTest { // ProtocolOrGovernorUpgrade proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }); // MaintenanceUpgrade - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: 0, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID - }); + proposalTypesData[1] = + ProposalValidator.ProposalTypeData({ requiredApprovals: 0, idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }); // CouncilMemberElections proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); // GovernanceFund proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); // CouncilBudget proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); return (proposalTypes, proposalTypesData); @@ -680,7 +678,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); + (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalTypes[i]); if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { assertEq(requiredApprovals, 0); } else { @@ -693,10 +691,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections ) { - assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); + assertEq(idInConfigurator, APPROVAL_VOTING_MODULE_ID); } else { // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID - assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); + assertEq(idInConfigurator, OPTIMISTIC_VOTING_MODULE_ID); } } } @@ -709,14 +707,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mismatched array with different length ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); - proposalTypesData[0] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 - }); - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 - }); + proposalTypesData[0] = + ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 0 }); + proposalTypesData[1] = + ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 1 }); vm.prank(owner); vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); @@ -790,7 +784,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -1046,7 +1040,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -1176,7 +1170,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(proposalId) ); @@ -2216,7 +2210,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -2309,7 +2303,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -2348,7 +2342,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -2455,7 +2449,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -2530,7 +2524,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I approvalVotingModule, governanceFundVotingModuleData, governanceFundProposalDescription, - uint8(governanceFundProposalType) + APPROVAL_VOTING_MODULE_ID ) ), abi.encode(uint256(expectedGovernanceFundHash)) @@ -2570,7 +2564,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I approvalVotingModule, councilBudgetVotingModuleData, councilBudgetProposalDescription, - uint8(councilBudgetProposalType) + APPROVAL_VOTING_MODULE_ID ) ), abi.encode(uint256(expectedCouncilBudgetHash)) @@ -2905,7 +2899,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -3012,7 +3006,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalVotingModule: newProposalTypeId + idInConfigurator: newProposalTypeId }); // Expect the ProposalTypeDataSet event to be emitted @@ -3022,16 +3016,16 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setProposalTypeData(proposalType, newData); - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); + (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newProposalTypeId); + assertEq(idInConfigurator, newProposalTypeId); } function testFuzz_setProposalTypeData_notOwner_reverts(address caller) public { vm.assume(caller != owner); ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, idInConfigurator: 0 }); vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); From 253cc6e8ff2e5132ac14ef7102479f991adf4060 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:54:55 -0300 Subject: [PATCH 30/73] fix: normalize validate functions (#456) * refactor: remove canApproveProposal and normalize validate functions * fix(test): handle invalid voting cycle edge case * fix: pre-pr --- .../governance/IProposalValidator.sol | 2 - .../snapshots/abi/ProposalValidator.json | 29 ----------- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 43 ++-------------- .../test/governance/ProposalValidator.t.sol | 51 +------------------ 5 files changed, 7 insertions(+), 122 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 090499667d3..ec63c98eaff 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -131,8 +131,6 @@ interface IProposalValidator is ISemver { function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate, bytes32 _proposalHash) external view returns (bool canApprove_); - function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 66b2363eda4..dc516cfa49b 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -90,35 +90,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_attestationUid", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "_delegate", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "_proposalHash", - "type": "bytes32" - } - ], - "name": "canApproveProposal", - "outputs": [ - { - "internalType": "bool", - "name": "canApprove_", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index ae6cdf304eb..3a11df4c0a0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xca95fa18ecb8d7d44043dd60c545719e526fdf487ea405c8321d25b92782bfd6", - "sourceCodeHash": "0xf0b71a440952d0a4a00a488f5b98da2010d1972297876ea8ade494d0885f7508" + "initCodeHash": "0x6bcced3e1050048ecc63fd4d9a86ec72e756de4cb328d29c357cc31c87834341", + "sourceCodeHash": "0x9380a09eeb5f12304baf0d45ea14e7bbd1b046d805429bfe0de970a7dfccd176" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 42242128349..30702c95efa 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -624,7 +624,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // validate the attestation - _validateTopDelegateAttestation(_attestationUid, _msgSender(), previousVotingCycle); + _validateTopDelegateAttestation(_attestationUid, previousVotingCycle); // store the approval proposal.delegateApprovals[_delegate] = true; @@ -633,30 +633,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalApproved(_proposalHash, _delegate); } - /// @notice Checks if a delegate can approve a proposal. - /// @dev Helper function for UI integration. - /// @param _attestationUid The UID of the attestation to check. - /// @param _delegate The delegate to check the attestation for. - /// @param _proposalHash The hash of the proposal to check the attestation for. - /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal( - bytes32 _attestationUid, - address _delegate, - bytes32 _proposalHash - ) - external - view - returns (bool canApprove_) - { - // TODO: this function should be fixed in OPT-957 - ProposalData storage proposal = _proposals[_proposalHash]; - if (proposal.votingCycle == 0) { - return false; - } - - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate, proposal.votingCycle - 1); - } - /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. /// @param _againstThreshold The threshold for the proposal to be against the total supply. /// @param _proposalDescription Description of the proposal. @@ -989,17 +965,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Validates the attestation data for a delegate that tries to approve a proposal. /// @dev Only acceptes attestations that does NOT include partial delegation. /// @param _attestationUid The UID of the attestation to validate. - /// @param _delegate The delegate to validate the attestation for. - /// @return canApprove_ True if the attestation is valid, false otherwise. - function _validateTopDelegateAttestation( - bytes32 _attestationUid, - address _delegate, - uint256 _lastVotingCycle - ) - internal - view - returns (bool canApprove_) - { + /// @param _lastVotingCycle The last voting cycle to validate against. + function _validateTopDelegateAttestation(bytes32 _attestationUid, uint256 _lastVotingCycle) internal view { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); VotingCycleData memory previousVotingCycleData = votingCycles[_lastVotingCycle]; if (previousVotingCycleData.startingTimestamp == 0) { @@ -1031,11 +998,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); // check if the attestation includes partial delegation or the recipient is not the caller - if (_includePartialDelegation || attestation.recipient != _delegate) { + if (_includePartialDelegation || attestation.recipient != _msgSender()) { revert ProposalValidator_InvalidAttestation(); } - - canApprove_ = true; } /// @notice Internal function to build proposal options with optional execution data. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 15eea020bd5..1407a1dc601 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1971,7 +1971,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ) public { - vm.assume(votingCycle != CYCLE_NUMBER); + vm.assume(votingCycle != CYCLE_NUMBER && votingCycle != 0); // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -2136,55 +2136,6 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { } } -/// @title ProposalValidator_CanApproveProposal_Test -/// @notice Tests for the canApproveProposal function -contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set the voting cycle data of the previous cycle - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); - - // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); - assertTrue(canApprove); - } - - function test_canApproveProposal_returnFalseRevert_succeeds( - bytes32 _attestationUid, - address _delegate, - bytes32 _proposalHash - ) - public - { - // Ensure the attestation uid is not one of the top delegates - vm.assume(_attestationUid != topDelegateAttestation_A); - - bool canApprove; - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(_attestationUid, _delegate, _proposalHash) returns (bool result_) { - canApprove = result_; - } catch { - canApprove = false; - } - - assertEq(canApprove, false); - } - - function test_canApproveProposal_returnFalseProposalNotFound_reverts(bytes32 _proposalHash) public { - validator.setProposalData( - _proposalHash, topDelegate_A, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, 0, 0 - ); - - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); - assertEq(canApprove, false); - } -} - /// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test /// @notice Happy path tests for moveToVoteProtocolOrGovernorUpgradeProposal function contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { From 332b1f3e49177773406b71855c37af7e46e6d173 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:07:02 -0300 Subject: [PATCH 31/73] chore: use fixed variable for contract version (#457) --- .../snapshots/abi/ProposalValidator.json | 2 +- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index dc516cfa49b..c1f63a7b24d 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -538,7 +538,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 3a11df4c0a0..215f92c69a8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x6bcced3e1050048ecc63fd4d9a86ec72e756de4cb328d29c357cc31c87834341", - "sourceCodeHash": "0x9380a09eeb5f12304baf0d45ea14e7bbd1b046d805429bfe0de970a7dfccd176" + "initCodeHash": "0x8f58c80a20e9f5e631d9451d86b2b5478a60879e58420f6788e75629f5863430", + "sourceCodeHash": "0x1d4ce0d6f868bd36418e7e760c81ecb033c15720e6a35f088ea135620fcae91a" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 30702c95efa..19500f835f0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -206,6 +206,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { CONSTANTS //////////////////////////////////////////////////////////////*/ + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + /// @notice The divisor used for percentage calculations in optimistic voting modules. /// @dev Represents 100% in basis points (10,000 = 100%). uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; @@ -238,12 +242,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) internal _proposals; - /// @notice Semantic version. - /// @custom:semver 1.0.0 - function version() public pure virtual returns (string memory) { - return "1.0.0"; - } - /// @notice Constructs the ProposalValidator contract. /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller From 2499ac1078068604b65e64ca2cd0dddfcc32c84d Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:57:29 -0300 Subject: [PATCH 32/73] fix: pp minors (#458) * chore: use fixed variable for contract version * refactor: rename proposalHash to proposalId for governor consistency * chore: move attestation schemas uids to initialize function * chore: specify target modules in submit functions * refactor: proper naming for modules settings * fix: pre-pr --- .../governance/IProposalValidator.sol | 30 +- .../snapshots/abi/ProposalValidator.json | 134 +++--- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 24 +- .../src/governance/ProposalValidator.sol | 202 +++++---- .../test/governance/ProposalValidator.t.sol | 413 +++++++++--------- 6 files changed, 399 insertions(+), 408 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index ec63c98eaff..291ba4833b0 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -34,19 +34,19 @@ interface IProposalValidator is ISemver { error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed proposer, string description, ProposalType proposalType ); event ProposalApproved( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed approver ); event ProposalMovedToVote( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed executor ); @@ -66,7 +66,7 @@ interface IProposalValidator is ISemver { ); event ProposalVotingModuleData( - bytes32 indexed proposalHash, + uint256 indexed proposalId, bytes encodedVotingModuleData ); @@ -109,7 +109,7 @@ interface IProposalValidator is ISemver { bytes32 _attestationUid, ProposalType _proposalType, uint256 _latestVotingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, @@ -117,7 +117,7 @@ interface IProposalValidator is ISemver { string memory _proposalDescription, bytes32 _attestationUid, uint256 _votingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function submitFundingProposal( uint128 _criteriaValue, @@ -127,20 +127,20 @@ interface IProposalValidator is ISemver { string memory _description, ProposalType _proposalType, uint256 _votingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + function approveProposal(uint256 _proposalId, bytes32 _attestationUid) external; function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function moveToVoteCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, string memory _proposalDescription - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function moveToVoteFundingProposal( uint128 _criteriaValue, @@ -149,7 +149,7 @@ interface IProposalValidator is ISemver { uint256[] memory _optionsAmounts, string memory _description, ProposalType _proposalType - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function setVotingCycleData( uint256 _cycleNumber, @@ -172,6 +172,8 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -188,9 +190,9 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function approvedProposerAttestationSchemaUid() external view returns (bytes32); - function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function topDelegatesAttestationSchemaUid() external view returns (bytes32); function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); @@ -204,8 +206,6 @@ interface IProposalValidator is ISemver { ); function __constructor__( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index c1f63a7b24d..b26b2647557 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -1,16 +1,6 @@ [ { "inputs": [ - { - "internalType": "bytes32", - "name": "_approvedProposerAttestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "_topDelegatesAttestationSchemaUid", - "type": "bytes32" - }, { "internalType": "contract IOptimismGovernor", "name": "_governor", @@ -20,19 +10,6 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "GOVERNOR", @@ -60,34 +37,34 @@ "type": "function" }, { - "inputs": [], - "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", - "outputs": [ + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + }, { "internalType": "bytes32", - "name": "", + "name": "_attestationUid", "type": "bytes32" } ], - "stateMutability": "view", + "name": "approveProposal", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [ - { - "internalType": "bytes32", - "name": "_proposalHash", - "type": "bytes32" - }, + "inputs": [], + "name": "approvedProposerAttestationSchemaUid", + "outputs": [ { "internalType": "bytes32", - "name": "_attestationUid", + "name": "", "type": "bytes32" } ], - "name": "approveProposal", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -135,6 +112,16 @@ "name": "_proposalDistributionThreshold", "type": "uint256" }, + { + "internalType": "bytes32", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", + "type": "bytes32" + }, { "internalType": "enum ProposalValidator.ProposalType[]", "name": "_proposalTypes", @@ -184,9 +171,9 @@ "name": "moveToVoteCouncilMemberElectionsProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -228,9 +215,9 @@ "name": "moveToVoteFundingProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -252,9 +239,9 @@ "name": "moveToVoteProtocolOrGovernorUpgradeProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -419,9 +406,9 @@ "name": "submitCouncilMemberElectionsProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -468,9 +455,9 @@ "name": "submitFundingProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -505,14 +492,27 @@ } ], "name": "submitUpgradeProposal", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "topDelegatesAttestationSchemaUid", "outputs": [ { "internalType": "bytes32", - "name": "proposalHash_", + "name": "", "type": "bytes32" } ], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -612,9 +612,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -644,9 +644,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -663,9 +663,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -719,9 +719,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": false, diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 215f92c69a8..70d743998b6 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x8f58c80a20e9f5e631d9451d86b2b5478a60879e58420f6788e75629f5863430", - "sourceCodeHash": "0x1d4ce0d6f868bd36418e7e760c81ecb033c15720e6a35f088ea135620fcae91a" + "initCodeHash": "0x4171ca5a66e60a2a3715dc3108d76f985eb16937d71197c74ee0465461e8f9fe", + "sourceCodeHash": "0xa95d144dad3bb25ef0ac0e4ae9039b1ee5304e86e9ef7c1726d263f63598f864" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index e8bbb446de6..2b847ae3993 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -36,30 +36,44 @@ }, { "bytes": "32", - "label": "proposalDistributionThreshold", + "label": "approvedProposerAttestationSchemaUid", "offset": 0, "slot": "101", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "topDelegatesAttestationSchemaUid", + "offset": 0, + "slot": "102", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "proposalDistributionThreshold", + "offset": 0, + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "102", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "103", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "104", - "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" + "slot": "106", + "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 19500f835f0..61db34720bb 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -80,7 +80,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the trying to move a proposal to vote outside of the accepted voting cycle. error ProposalValidator_InvalidVotingCycle(); - /// @notice Thrown when the proposalId returned by the Governor is not the same as the proposalHash. + /// @notice Thrown when the proposalId returned by the Governor does not match the expected proposalId. error ProposalValidator_ProposalIdMismatch(); /// @notice Thrown when the caller is not the proposer. @@ -100,23 +100,23 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a new proposal is submitted. - /// @param proposalHash The hash of the submitted proposal. + /// @param proposalId The ID of the submitted proposal. /// @param proposer The address that submitted the proposal. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. event ProposalSubmitted( - bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + uint256 indexed proposalId, address indexed proposer, string description, ProposalType proposalType ); /// @notice Emitted when a delegate approves a proposal. - /// @param proposalHash The hash of the approved proposal. + /// @param proposalId The ID of the approved proposal. /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + event ProposalApproved(uint256 indexed proposalId, address indexed approver); /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalHash The hash of the proposal moved to vote. + /// @param proposalId The ID of the proposal moved to vote. /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. @@ -138,9 +138,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. - /// @param proposalHash The hash of the submitted proposal. + /// @param proposalId The ID of the submitted proposal. /// @param encodedVotingModuleData The encoded voting module data. - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + event ProposalVotingModuleData(uint256 indexed proposalId, bytes encodedVotingModuleData); /*////////////////////////////////////////////////////////////// STRUCTS @@ -166,6 +166,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. /// @param idInConfigurator The proposal type ID used to get the voting module from the configurator. + /// @dev Based on the spec document, funding and council member elections proposals are + /// configured for the ApprovalVotingModule, while the upgrade proposals are configured for the + /// OptimisticVotingModule. + /// Any change on the module used for proposals would require the Validator to be upgraded. struct ProposalTypeData { uint256 requiredApprovals; uint8 idInConfigurator; @@ -218,17 +222,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { STATE VARIABLES //////////////////////////////////////////////////////////////*/ + /// @notice The Optimism Governor contract that will handle the voting phase. + IOptimismGovernor public immutable GOVERNOR; + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { proposalType: uint8, date: string } - bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public approvedProposerAttestationSchemaUid; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is part of the top100 delegates. - bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; - - /// @notice The Optimism Governor contract that will handle the voting phase. - IOptimismGovernor public immutable GOVERNOR; + bytes32 public topDelegatesAttestationSchemaUid; /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -239,23 +243,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of proposal types to their corresponding data. mapping(ProposalType => ProposalTypeData) public proposalTypesData; - /// @notice Mapping of proposal hash to their corresponding proposal data. - mapping(bytes32 => ProposalData) internal _proposals; + /// @notice Mapping of proposal ID to their corresponding proposal data. + mapping(uint256 => ProposalData) internal _proposals; /// @notice Constructs the ProposalValidator contract. - /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. - /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller - /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. - constructor( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor - ) - ReinitializableBase(1) - { - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; - TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; + constructor(IOptimismGovernor _governor) ReinitializableBase(1) { GOVERNOR = _governor; _disableInitializers(); } @@ -276,6 +269,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -286,6 +281,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } + approvedProposerAttestationSchemaUid = _approvedProposerAttestationSchemaUid; + topDelegatesAttestationSchemaUid = _topDelegatesAttestationSchemaUid; + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -305,7 +303,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). /// @param _latestVotingCycle The latest voting cycle number. Even though the upgrade proposal can be submitted /// outside of a voting cycle, we still need the latest voting cycle number to validate top delegates attestations. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, @@ -314,7 +312,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _latestVotingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Validate proposal type is valid for upgrade proposals if (_proposalType != ProposalType.ProtocolOrGovernorUpgrade && _proposalType != ProposalType.MaintenanceUpgrade) @@ -360,11 +358,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingModule = proposalTypeConfig.module; } - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Prevent duplicate proposals if (proposal.proposer != address(0)) { @@ -372,7 +370,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -381,8 +379,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = _proposalType; proposal.votingCycle = _latestVotingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _proposalDescription, _proposalType); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) if (_proposalType == ProposalType.MaintenanceUpgrade) { @@ -392,12 +390,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator ); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } } @@ -408,7 +406,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _votingCycle The voting cycle number the proposal is targetted for. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, @@ -417,7 +415,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Validate voting cycle exists and is not in the past VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; @@ -444,7 +442,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections @@ -452,7 +450,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: 0 // No budget amount for elections }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( @@ -465,19 +463,19 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidVotingModule(); } - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; - // Prevent duplicate proposals with same hash + // Prevent duplicate proposals with same ID if (proposal.proposer != address(0)) { revert ProposalValidator_ProposalAlreadySubmitted(); } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -486,8 +484,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = ProposalType.CouncilMemberElections; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); } /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and @@ -503,7 +501,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). /// @param _votingCycle The voting cycle number the proposal is targetted for. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, @@ -514,7 +512,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { @@ -543,7 +541,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -551,7 +549,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: uint128(totalBudget) }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( @@ -564,18 +562,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidVotingModule(); } - // Generate unique proposal hash - proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; - // Prevent duplicate proposals with same hash + // Prevent duplicate proposals with same ID if (proposal.proposer != address(0)) { revert ProposalValidator_ProposalAlreadySubmitted(); } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -584,17 +582,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _description, _proposalType); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _description, _proposalType); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); } /// @notice Approves a proposal before being moved for voting. /// @dev This function should only be called by the top delegates. - /// @param _proposalHash The hash of the proposal to approve + /// @param _proposalId The ID of the proposal to approve /// @param _attestationUid The UID of the attestation for the delegate to approve the proposal - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external { + function approveProposal(uint256 _proposalId, bytes32 _attestationUid) external { address _delegate = _msgSender(); - ProposalData storage proposal = _proposals[_proposalHash]; + ProposalData storage proposal = _proposals[_proposalId]; // check if the proposal exists // proposal.votingCycle should never be 0, voting cycles already exist before the ProposalValidator is deployed // and should be set by the OP Foundation @@ -628,25 +626,25 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.delegateApprovals[_delegate] = true; proposal.approvalCount++; - emit ProposalApproved(_proposalHash, _delegate); + emit ProposalApproved(_proposalId, _delegate); } /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. /// @param _againstThreshold The threshold for the proposal to be against the total supply. /// @param _proposalDescription Description of the proposal. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Configure optimistic proposal settings - IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); - bytes memory proposalVotingModuleData = abi.encode(settings); + bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Retrieve the ID to use in the proposal type configurator uint8 idInConfigurator = proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].idInConfigurator; @@ -656,11 +654,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist and be valid if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { @@ -688,33 +686,33 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Moves a council member elections proposal to vote by proposing it on the Governor. /// @param _criteriaValue The number of top choices that can pass the voting. /// @param _optionsDescriptions The strings of the different options that can be voted. /// @param _proposalDescription Description of the proposal. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, string memory _proposalDescription ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), @@ -722,7 +720,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: 0 }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); ProposalType _proposalType = ProposalType.CouncilMemberElections; @@ -733,11 +731,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist and be valid if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { @@ -774,12 +772,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Moves a funding proposal to vote by proposing it on the Governor. @@ -793,7 +791,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, @@ -803,7 +801,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ProposalType _proposalType ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { @@ -815,7 +813,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval module settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -823,7 +821,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: uint128(totalBudget) }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Retrieve the ID to use in the proposal type configurator uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; @@ -832,10 +830,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { @@ -874,12 +872,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Sets the data of a voting cycle. @@ -953,7 +951,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( - attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID + attestation.attester != owner() || attestation.schema != approvedProposerAttestationSchemaUid || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); @@ -961,7 +959,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Validates the attestation data for a delegate that tries to approve a proposal. - /// @dev Only acceptes attestations that does NOT include partial delegation. + /// @dev Only accepts attestations that do NOT include partial delegation. /// @param _attestationUid The UID of the attestation to validate. /// @param _lastVotingCycle The last voting cycle to validate against. function _validateTopDelegateAttestation(bytes32 _attestationUid, uint256 _lastVotingCycle) internal view { @@ -977,7 +975,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // check if the schema is correct - if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { + if (attestation.schema != topDelegatesAttestationSchemaUid) { revert ProposalValidator_InvalidAttestationSchema(); } @@ -1057,11 +1055,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } } - /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. + /// @notice Calculate `proposalId` based on `module`, `proposalData` and `descriptionHash`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. /// @param _descriptionHash The hash of the proposal description. - /// @return The hash of the proposal. + /// @return The proposal ID as uint256. function _hashProposalWithModule( address _module, bytes memory _proposalData, @@ -1069,9 +1067,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ) internal view - returns (bytes32) + returns (uint256) { - return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); + return uint256(keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash))); } /// @notice Private function to set the voting cycle data and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 1407a1dc601..a307afbb918 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -33,15 +33,9 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest -/// @notice A test contract that exposes the private _hashProposal function +/// @notice A test contract that exposes the private _hashProposalWithModule function contract ProposalValidatorForTest is ProposalValidator { - constructor( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor - ) - ProposalValidator(_approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid, _governor) - { } + constructor(IOptimismGovernor _governor) ProposalValidator(_governor) { } function hashProposalWithModule( address _module, @@ -50,13 +44,13 @@ contract ProposalValidatorForTest is ProposalValidator { ) public view - returns (bytes32) + returns (uint256) { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } /// @notice Exposes proposal data for testing - function getProposalData(bytes32 _proposalHash) + function getProposalData(uint256 _proposalId) public view returns ( @@ -67,14 +61,14 @@ contract ProposalValidatorForTest is ProposalValidator { uint256 votingCycle_ ) { - ProposalData storage proposal = _proposals[_proposalHash]; + ProposalData storage proposal = _proposals[_proposalId]; return ( proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle ); } function setProposalData( - bytes32 _proposalHash, + uint256 _proposalId, address _proposer, ProposalType _proposalType, bool _movedToVote, @@ -83,20 +77,20 @@ contract ProposalValidatorForTest is ProposalValidator { ) public { - _proposals[_proposalHash].proposer = _proposer; - _proposals[_proposalHash].proposalType = _proposalType; - _proposals[_proposalHash].movedToVote = _movedToVote; - _proposals[_proposalHash].approvalCount = _approvalCount; - _proposals[_proposalHash].votingCycle = _votingCycle; + _proposals[_proposalId].proposer = _proposer; + _proposals[_proposalId].proposalType = _proposalType; + _proposals[_proposalId].movedToVote = _movedToVote; + _proposals[_proposalId].approvalCount = _approvalCount; + _proposals[_proposalId].votingCycle = _votingCycle; } - function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { - _proposals[_proposalHash].delegateApprovals[_delegate] = true; + function mockApproveProposal(uint256 _proposalId, address _delegate) public { + _proposals[_proposalId].delegateApprovals[_delegate] = true; } /// @notice Check if a delegate has approved a proposal - function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { - return _proposals[_proposalHash].delegateApprovals[_delegate]; + function hasDelegateApproved(uint256 _proposalId, address _delegate) public view returns (bool hasApproved_) { + return _proposals[_proposalId].delegateApprovals[_delegate]; } } @@ -116,6 +110,8 @@ contract ProposalValidator_Init is CommonTest { uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; uint64 public constant ATT_EXPIRATION_TIME = 10 days; + bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; address owner; address user; @@ -129,17 +125,15 @@ contract ProposalValidator_Init is CommonTest { ProposalValidatorForTest public impl; IOptimismGovernor public governor; IProposalTypesConfigurator public proposalTypesConfigurator; - bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; - bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; event ProposalSubmitted( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed proposer, string description, ProposalValidator.ProposalType proposalType ); - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event ProposalApproved(uint256 indexed proposalId, address indexed approver); + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); event MinimumVotingPowerSet(uint256 newMinimumVotingPower); event VotingCycleDataSet( uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit @@ -148,7 +142,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator ); - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + event ProposalVotingModuleData(uint256 indexed proposalId, bytes encodedVotingModuleData); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -321,7 +315,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -329,7 +323,7 @@ contract ProposalValidator_Init is CommonTest { budgetAmount: uint128(totalBudget) }); - return abi.encode(options, settings); + return abi.encode(options, approvalSettings); } /// @notice Helper function to construct voting module data for council elections @@ -360,7 +354,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), @@ -368,15 +362,15 @@ contract ProposalValidator_Init is CommonTest { budgetAmount: 0 }); - return abi.encode(options, settings); + return abi.encode(options, approvalSettings); } /// @notice Helper function to construct voting module data for upgrade proposals function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { - IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); - return abi.encode(settings); + return abi.encode(optimisticSettings); } /// @notice Helper function to create a proposal for move to vote @@ -386,17 +380,17 @@ contract ProposalValidator_Init is CommonTest { string memory proposalDescription ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { - // Calculate expected proposal hash + // Calculate expected proposal ID votingModuleData_ = _constructOptimisticVotingModuleData(againstThreshold); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); // 1 vote as default for being able to move to vote validator.setProposalData( - proposalHash_, + proposalId_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, @@ -413,15 +407,15 @@ contract ProposalValidator_Init is CommonTest { string memory proposalDescription ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { votingModuleData_ = _constructCouncilElectionVotingModuleData(optionsDescriptions, criteriaValue); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); validator.setProposalData( - proposalHash_, + proposalId_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, @@ -441,17 +435,15 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType proposalType ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { votingModuleData_ = _constructFundingVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData( - proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER - ); + validator.setProposalData(proposalId_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); } /// @notice Helper function to setup proposal types configurator mocks @@ -524,9 +516,7 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor - ); + impl = new ProposalValidatorForTest(governor); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -541,6 +531,8 @@ contract ProposalValidator_Init is CommonTest { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -634,9 +626,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor - ); + impl = new ProposalValidatorForTest(governor); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -658,6 +648,8 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -725,6 +717,8 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -764,17 +758,15 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init // Create attestation for the proposal bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -786,25 +778,25 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, proposer); + emit ProposalMovedToVote(expectedId, proposer); vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( + uint256 proposalId = validator.submitUpgradeProposal( againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -813,7 +805,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -839,34 +831,32 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init // Create attestation for the proposal bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( + uint256 proposalId = validator.submitUpgradeProposal( againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -875,7 +865,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -1019,17 +1009,15 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -1042,7 +1030,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); } @@ -1070,16 +1058,16 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1149,20 +1137,18 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - vm.assume(proposalId != uint256(expectedHash)); // Ensure proposalId is different from expectedHash + vm.assume(proposalId != expectedId); // Ensure proposalId is different from expectedId _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Mock the proposeWithModule call to return a different proposalId @@ -1210,37 +1196,35 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Expect ProposalSubmitted event vm.expectEmit(address(validator)); emit ProposalSubmitted( - expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections + expectedId, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections ); // Expect ProposalVotingModuleData event vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + uint256 proposalId = validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -1249,7 +1233,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); assertEq( @@ -1340,17 +1324,15 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop } function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -1373,16 +1355,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop } function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1517,35 +1499,33 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init amounts[i] = amount; } - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, criteriaValue); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Expect ProposalSubmitted event vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, description, proposalType); + emit ProposalSubmitted(expectedId, proposer, description, proposalType); // Expect ProposalVotingModuleData event vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(proposer); - bytes32 proposalHash = validator.submitFundingProposal( + uint256 proposalId = validator.submitFundingProposal( criteriaValue, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -1554,7 +1534,7 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -1747,17 +1727,15 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -1787,16 +1765,16 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1887,29 +1865,29 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); } - function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Ensure the proposal hash is not 0 - vm.assume(_proposalHash != bytes32(0)); + function test_approveProposal_succeeds(uint256 _proposalId, uint8 proposalTypeValue) public { + // Ensure the proposal ID is not 0 + vm.assume(_proposalId != 0); // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect event to be emitted when approving vm.expectEmit(address(validator)); - emit ProposalApproved(_proposalHash, topDelegate_A); + emit ProposalApproved(_proposalId, topDelegate_A); // Approve the proposal, use the attestation of the top delegate that was created in setUp vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); // Check that the proposal data has been updated - assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); + assertTrue(validator.hasDelegateApproved(_proposalId, topDelegate_A)); - (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalId); assertEq(approvalCount, 1); } } @@ -1921,15 +1899,15 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { super.setUp(); } - function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + function test_approveProposal_proposalDoesNotExist_reverts(uint256 _proposalId) public { // There is no stored proposal data so this will revert vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyApproved_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -1938,17 +1916,17 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal as already approved by the top delegate - validator.mockApproveProposal(_proposalHash, topDelegate_A); + validator.mockApproveProposal(_proposalId, topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyMovedToVote_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -1957,33 +1935,36 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists and set movedToVote to true - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidVotingCycle_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, uint256 votingCycle ) public { vm.assume(votingCycle != CYCLE_NUMBER && votingCycle != 0); + vm.assume(votingCycle != CYCLE_NUMBER + 1); // Avoid existing cycle + // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, votingCycle); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, votingCycle); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } - function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + function test_approveProposal_invalidSchema_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -2011,22 +1992,22 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _invalidAttestationUid); + validator.approveProposal(_proposalId, _invalidAttestationUid); } - function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + function test_approveProposal_attestationRevoked_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2042,11 +2023,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidAttestationCaller_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, address _caller ) @@ -2060,7 +2041,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.assume(_caller != topDelegate_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2068,11 +2049,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(_caller); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidAttestationPartialDelegation_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -2082,7 +2063,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2106,11 +2087,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + validator.approveProposal(_proposalId, _attestationUidWithPartialDelegation); } function test_approveProposal_nonExistentAttestation_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, bytes32 _nonExistentAttestationUid ) @@ -2124,7 +2105,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.assume(_nonExistentAttestationUid != topDelegateAttestation_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2132,7 +2113,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + validator.approveProposal(_proposalId, _nonExistentAttestationUid); } } @@ -2143,12 +2124,12 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P string proposalDescription = "Test proposal"; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes votingModuleData; - bytes32 expectedHash; + uint256 expectedId; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = + (expectedId, votingModuleData) = _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); } @@ -2163,19 +2144,19 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalMovedToVote(expectedId, approvedProposer); // Move to vote vm.prank(approvedProposer); validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } } @@ -2185,12 +2166,12 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail string proposalDescription = "Test proposal"; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes votingModuleData; - bytes32 expectedHash; + uint256 expectedId; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = + (expectedId, votingModuleData) = _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); } @@ -2208,7 +2189,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 _againstThreshold) public { - // This will generate a different proposal hash which will make the proposal type wrong + // This will generate a different proposal ID which will make the proposal type wrong vm.assume(_againstThreshold != againstThreshold); // Mock the proposal types configurator call @@ -2221,7 +2202,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -2236,15 +2217,15 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(approvedProposer); validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(uint256 _randomId) public { + vm.assume(_randomId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -2256,7 +2237,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2268,7 +2249,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; uint128 criteriaValue = 1; - bytes32 expectedHash; + uint256 expectedId; bytes votingModuleData; string proposalDescription = "Test proposal"; string[] optionsDescriptions = new string[](2); @@ -2279,7 +2260,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop // Create a proposal for move to vote with 1 top choice and 2 options optionsDescriptions[0] = "Option 1"; optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); } @@ -2295,12 +2276,12 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalMovedToVote(expectedId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2308,7 +2289,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } } @@ -2318,7 +2299,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is uint128 criteriaValue = 1; string proposalDescription = "Test proposal"; string[] optionsDescriptions = new string[](2); - bytes32 expectedHash; + uint256 expectedId; bytes votingModuleData; function setUp() public override { @@ -2327,7 +2308,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is // Create a proposal for move to vote with 1 top choice and 2 options optionsDescriptions[0] = "Option 1"; optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); } @@ -2344,7 +2325,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is } function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { - // This will generate a different proposal hash which will make the proposal type wrong + // This will generate a different proposal ID which will make the proposal type wrong uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp // Mock the proposal types configurator call @@ -2357,7 +2338,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2369,7 +2350,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2389,8 +2370,8 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(uint256 _randomId) public { + vm.assume(_randomId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2402,7 +2383,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2421,8 +2402,8 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I string[] optionsDescriptions = new string[](2); address[] optionsRecipients = new address[](2); uint256[] optionsAmounts = new uint256[](2); - bytes32 expectedGovernanceFundHash; - bytes32 expectedCouncilBudgetHash; + uint256 expectedGovernanceFundId; + uint256 expectedCouncilBudgetId; bytes governanceFundVotingModuleData; bytes councilBudgetVotingModuleData; @@ -2442,7 +2423,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I optionsAmounts[1] = 200 ether; // Create one proposal for each type - (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + (expectedGovernanceFundId, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2451,7 +2432,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I governanceFundProposalDescription, governanceFundProposalType ); - (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + (expectedCouncilBudgetId, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2478,12 +2459,12 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I APPROVAL_VOTING_MODULE_ID ) ), - abi.encode(uint256(expectedGovernanceFundHash)) + abi.encode(uint256(expectedGovernanceFundId)) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + emit ProposalMovedToVote(expectedGovernanceFundId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2498,7 +2479,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I ); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundId); assertTrue(movedToVote, "Proposal should be in voting"); } @@ -2518,12 +2499,12 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I APPROVAL_VOTING_MODULE_ID ) ), - abi.encode(uint256(expectedCouncilBudgetHash)) + abi.encode(uint256(expectedCouncilBudgetId)) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + emit ProposalMovedToVote(expectedCouncilBudgetId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2548,8 +2529,8 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string[] optionsDescriptions; address[] optionsRecipients; uint256[] optionsAmounts; - bytes32 governanceFundExpectedHash; - bytes32 councilBudgetExpectedHash; + uint256 governanceFundExpectedId; + uint256 councilBudgetExpectedId; bytes governanceFundVotingModuleData; bytes councilBudgetVotingModuleData; @@ -2557,7 +2538,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat super.setUp(); (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(1); - (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + (governanceFundExpectedId, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2566,7 +2547,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat governanceFundProposalDescription, governanceFundProposalType ); - (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + (councilBudgetExpectedId, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2600,7 +2581,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ) public { - // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal ID it will // not find the proposal vm.assume(_criteriaValue != criteriaValue); @@ -2642,13 +2623,13 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat if (validProposalType == governanceFundProposalType) { // Set proposal data proposal type to a different value validator.setProposalData( - governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + governanceFundExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data proposal type to a different value validator.setProposalData( - councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + councilBudgetExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); proposalDescription = councilBudgetProposalDescription; } @@ -2676,13 +2657,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string memory proposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData( - governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER - ); + validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); proposalDescription = councilBudgetProposalDescription; } @@ -2704,11 +2683,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string memory proposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data movedToVote to true - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data movedToVote to true - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); proposalDescription = councilBudgetProposalDescription; } @@ -2822,11 +2801,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( uint8 _proposalTypeValue, - bytes32 _randomHash + uint256 _randomId ) public { - vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + vm.assume(_randomId != governanceFundExpectedId && _randomId != councilBudgetExpectedId); // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); @@ -2852,7 +2831,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2995,11 +2974,11 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init public view { - bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); - bytes32 expectedHash = - keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash)); + uint256 id = validator.hashProposalWithModule(module, proposalData, descriptionHash); + uint256 expectedId = + uint256(keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash))); - assertEq(hash, expectedHash); + assertEq(id, expectedId); } function test_hashProposalWithModule_differentInputs_succeeds() public { @@ -3008,9 +2987,9 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init bytes memory data = abi.encode("data"); bytes32 descHash = keccak256("desc"); - bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); - bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); + uint256 id1 = validator.hashProposalWithModule(module1, data, descHash); + uint256 id2 = validator.hashProposalWithModule(module2, data, descHash); - assertTrue(hash1 != hash2); + assertTrue(id1 != id2); } } From 39b6ca9e13be417b59d715e95fb9fba6c7aa025e Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:20:27 +0300 Subject: [PATCH 33/73] fix: descrepancies (#464) * fix: improve comment * fix: make schemas immutable again * fix: improve code * fix: validation check order * fix: add move to vote safety check * fix: pre-pr --- .../governance/IProposalValidator.sol | 10 +- .../snapshots/abi/ProposalValidator.json | 72 +++++++------- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 22 +---- .../src/governance/ProposalValidator.sol | 99 ++++++++++++------- .../test/governance/ProposalValidator.t.sol | 57 +++++++++-- 6 files changed, 160 insertions(+), 104 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 291ba4833b0..84518b450dc 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -172,8 +172,6 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -190,9 +188,9 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function approvedProposerAttestationSchemaUid() external view returns (bytes32); + function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function topDelegatesAttestationSchemaUid() external view returns (bytes32); + function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); @@ -206,6 +204,8 @@ interface IProposalValidator is ISemver { ); function __constructor__( - IOptimismGovernor _governor + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index b26b2647557..f7c9631976a 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -5,11 +5,34 @@ "internalType": "contract IOptimismGovernor", "name": "_governor", "type": "address" + }, + { + "internalType": "bytes32", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", + "type": "bytes32" } ], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "GOVERNOR", @@ -36,6 +59,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -54,19 +90,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "approvedProposerAttestationSchemaUid", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", @@ -112,16 +135,6 @@ "name": "_proposalDistributionThreshold", "type": "uint256" }, - { - "internalType": "bytes32", - "name": "_approvedProposerAttestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "_topDelegatesAttestationSchemaUid", - "type": "bytes32" - }, { "internalType": "enum ProposalValidator.ProposalType[]", "name": "_proposalTypes", @@ -502,19 +515,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "topDelegatesAttestationSchemaUid", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 70d743998b6..5d029bed231 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x4171ca5a66e60a2a3715dc3108d76f985eb16937d71197c74ee0465461e8f9fe", - "sourceCodeHash": "0xa95d144dad3bb25ef0ac0e4ae9039b1ee5304e86e9ef7c1726d263f63598f864" + "initCodeHash": "0xd96122c73104bff67f8493e0e0d9d7aeeb6a631ecd52d5572a8a21b724591ad9", + "sourceCodeHash": "0x857b3e452c59b2263d812868916e20aafb81380f1438616d913141ae45a6b806" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 2b847ae3993..167b206ea9c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,46 +34,32 @@ "slot": "52", "type": "uint256[49]" }, - { - "bytes": "32", - "label": "approvedProposerAttestationSchemaUid", - "offset": 0, - "slot": "101", - "type": "bytes32" - }, - { - "bytes": "32", - "label": "topDelegatesAttestationSchemaUid", - "offset": 0, - "slot": "102", - "type": "bytes32" - }, { "bytes": "32", "label": "proposalDistributionThreshold", "offset": 0, - "slot": "103", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "104", + "slot": "102", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "105", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 61db34720bb..01c6e0d28c3 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -134,7 +134,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param idInConfigurator The proposal type ID. + /// @param idInConfigurator The proposal type ID in the ProposalTypesConfigurator contract. event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. @@ -228,11 +228,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { proposalType: uint8, date: string } - bytes32 public approvedProposerAttestationSchemaUid; + bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is part of the top100 delegates. - bytes32 public topDelegatesAttestationSchemaUid; + bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -248,8 +248,22 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Constructs the ProposalValidator contract. /// @param _governor The Optimism Governor contract address. - constructor(IOptimismGovernor _governor) ReinitializableBase(1) { + /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in the Ethereum Attestation Service + /// for checking if the caller + /// is an approved proposer. + /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in the Ethereum Attestation Service for + /// checking if the caller + /// is part of the top100 delegates. + constructor( + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid + ) + ReinitializableBase(1) + { GOVERNOR = _governor; + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; _disableInitializers(); } @@ -269,8 +283,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -281,9 +293,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - approvedProposerAttestationSchemaUid = _approvedProposerAttestationSchemaUid; - topDelegatesAttestationSchemaUid = _topDelegatesAttestationSchemaUid; - _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -452,17 +461,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator; + // Get the module address from the configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator); - address votingModule = proposalTypeConfig.module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); // Validate voting module exists if (bytes(proposalTypeConfig.name).length == 0) { revert ProposalValidator_InvalidVotingModule(); } + address votingModule = proposalTypeConfig.module; + // Generate unique proposal ID proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); @@ -551,15 +563,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].idInConfigurator); - address votingModule = proposalTypeConfig.module; + address votingModule; + { + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); - // Validate voting module exists - if (bytes(proposalTypeConfig.name).length == 0) { - revert ProposalValidator_InvalidVotingModule(); + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } + + votingModule = proposalTypeConfig.module; } // Generate unique proposal ID @@ -707,13 +725,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (uint256 proposalId_) { + uint256 optionsLength = _optionsDescriptions.length; + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(_optionsDescriptions.length), + maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: _criteriaValue, @@ -808,6 +831,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidFundingProposalType(); } + uint256 optionsLength = _optionsDescriptions.length; + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); @@ -850,18 +879,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyMovedToVote(); } - // Check if proposal can be moved to vote - VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - if ( - votingCycleData.startingTimestamp > block.timestamp - || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp - ) { - revert ProposalValidator_InvalidVotingCycle(); - } + { + // Check if proposal can be moved to vote + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + if ( + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp + ) { + revert ProposalValidator_InvalidVotingCycle(); + } - // Check if total budget is within the voting cycle distribution limit - if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { - revert ProposalValidator_ExceedsDistributionThreshold(); + // Check if total budget is within the voting cycle distribution limit + if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } } // Move proposal to vote @@ -951,7 +982,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( - attestation.attester != owner() || attestation.schema != approvedProposerAttestationSchemaUid + attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); @@ -975,7 +1006,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // check if the schema is correct - if (attestation.schema != topDelegatesAttestationSchemaUid) { + if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { revert ProposalValidator_InvalidAttestationSchema(); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index a307afbb918..81d24cb0c27 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -35,7 +35,13 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest /// @notice A test contract that exposes the private _hashProposalWithModule function contract ProposalValidatorForTest is ProposalValidator { - constructor(IOptimismGovernor _governor) ProposalValidator(_governor) { } + constructor( + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid + ) + ProposalValidator(_governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) + { } function hashProposalWithModule( address _module, @@ -516,7 +522,9 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest(governor); + impl = new ProposalValidatorForTest( + governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -531,8 +539,6 @@ contract ProposalValidator_Init is CommonTest { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -626,7 +632,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest(governor); + impl = new ProposalValidatorForTest( + governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -648,8 +656,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -717,8 +723,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -2336,6 +2340,16 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); } + function test_moveToVoteCouncilMemberElectionsProposal_invalidOptionsLength_reverts() public { + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](0), proposalDescription); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](256), proposalDescription); + } + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); @@ -2649,6 +2663,31 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } + function test_moveToVoteFundingProposal_invalidOptionsLength_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, new string[](0), optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, new string[](256), optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); From 96371dd6053b620e6a3ecc24261872bb6f26c1f9 Mon Sep 17 00:00:00 2001 From: OneTony Date: Mon, 28 Jul 2025 22:03:34 +0300 Subject: [PATCH 34/73] fix: add total budget overflow check --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 9 +++- .../test/governance/ProposalValidator.t.sol | 49 +++++++++++++++++++ 5 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 84518b450dc..5208757dfc4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -31,6 +31,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); error ProposalValidator_InvalidVotingModule(); + error ProposalValidator_InvalidTotalBudget(); error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index f7c9631976a..f2305002325 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -829,6 +829,11 @@ "name": "ProposalValidator_InvalidProposer", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidTotalBudget", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidUpgradeProposalType", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 5d029bed231..f7ab1bedf50 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -176,8 +176,8 @@ "sourceCodeHash": "0x18f43b227decd0f2a895b8b55e23fa6a47706697c272bbf2482b3f912be446e1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd96122c73104bff67f8493e0e0d9d7aeeb6a631ecd52d5572a8a21b724591ad9", - "sourceCodeHash": "0x857b3e452c59b2263d812868916e20aafb81380f1438616d913141ae45a6b806" + "initCodeHash": "0xfd63a8de6795e33cba8b47ce2cab24f09a003065292d74bdbe236161335b02ba", + "sourceCodeHash": "0xa774418504002f9f2aaff0333a534c65a46e8bb9d9d1e9408fbe1b3204b10462" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 01c6e0d28c3..a1d9a76d09c 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -89,9 +89,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the proposal is invalid trying to move to vote. error ProposalValidator_InvalidProposal(); - /// @notice Thrown when the voting module address is invalid (zero address). + /// @notice Thrown when the voting module address is invalid. error ProposalValidator_InvalidVotingModule(); + /// @notice Thrown when the total budget is invalid (must be > 0 and <= uint128 max). + error ProposalValidator_InvalidTotalBudget(); + /// @notice Thrown when the attestation was created after the last voting cycle. error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); @@ -1084,6 +1087,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { description: _optionDescriptions[i] }); } + + if (totalBudget_ > type(uint128).max) { + revert ProposalValidator_InvalidTotalBudget(); + } } /// @notice Calculate `proposalId` based on `module`, `proposalData` and `descriptionHash`. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 81d24cb0c27..be1305762c1 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1855,6 +1855,26 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } + + function test_submitFundingProposal_invalidTotalBudget_reverts(uint8 proposalTypeValue, uint256 _amount) public { + _amount = bound(_amount, type(uint136).max, type(uint192).max); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + vm.prank(owner); + validator.setProposalDistributionThreshold(type(uint256).max); + + amounts[0] = _amount; + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidTotalBudget.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_ApproveProposal_Test @@ -2688,6 +2708,35 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } + function test_moveToVoteFundingProposal_invalidTotalBudget_reverts( + uint8 _proposalTypeValue, + uint256 _amount + ) + public + { + _amount = bound(_amount, type(uint136).max, type(uint192).max); + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + vm.prank(owner); + validator.setProposalDistributionThreshold(type(uint256).max); + + optionsAmounts[0] = _amount; + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidTotalBudget.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); From 55edad3b74083c5eb7deeb281468875f32151eba Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 6 May 2025 13:05:08 -0300 Subject: [PATCH 35/73] feat: add proposal validator (#367) * feat: add initial interface and logic * refactor: remove installed governor submodule * chore: remove xERC20 * feat: add proposal routing full flow * feat: check voting power and required proposals * refactor: rename to ProposalValidator * feat: add EAS validation for certain Proposal Types * chore: fix attestation schema approved address naming Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: remove management functions * chore: run pre-pr * refacto: follow style guide for function parameters and return variables * docs: add natspec, remove unused errors * chore: remove management functions from interface * chore: make voting token immutable * perf: make governor immutable * feat: add validator management functions * chore: add comments for imports in ProposalValidator * test: add unit tests * fix: semgrep warnings * chore: rename MaintenanceUpgradeProposals --> MaintenanceUpgrade * chore(semgrep): add excluded governance files * chore: fix coding style * chore: add ImmutableProposalTypeData * chore: improve errors naming * docs: improve natspec Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * docs: add technical explanation on attestation validation function * feat: add _proposalTypeData mapping * chore: keep private functions consistency * chore: improve required attestation naming * docs: improve documents legibility Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: fix immutable variables coding style * chore: explain governor external call * docs: explicit Validator/Governor contract interaction in events natspec --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .semgrep/rules/sol-rules.yaml | 6 + .../governance/IOptimismGovernor.sol | 20 + .../governance/IProposalValidator.sol | 125 +++++ .../snapshots/abi/ProposalValidator.json | 527 ++++++++++++++++++ .../storageLayout/ProposalValidator.json | 58 ++ .../src/governance/ProposalValidator.sol | 405 ++++++++++++++ .../src/governance/VotingModule.sol | 73 +++ .../test/governance/ProposalValidator.t.sol | 441 +++++++++++++++ 8 files changed, 1655 insertions(+) create mode 100644 packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol create mode 100644 packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol create mode 100644 packages/contracts-bedrock/snapshots/abi/ProposalValidator.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json create mode 100644 packages/contracts-bedrock/src/governance/ProposalValidator.sol create mode 100644 packages/contracts-bedrock/src/governance/VotingModule.sol create mode 100644 packages/contracts-bedrock/test/governance/ProposalValidator.t.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index bea5f9b6b4e..d77eb17a8a4 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -125,6 +125,7 @@ rules: - packages/contracts-bedrock/src/universal/WETH98.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol - packages/contracts-bedrock/src/governance/GovernanceToken.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -139,6 +140,7 @@ rules: - packages/contracts-bedrock/test/safe-tools - packages/contracts-bedrock/scripts/libraries/Solarray.sol - packages/contracts-bedrock/scripts/interfaces/IGnosisSafe.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - id: sol-style-doc-comment languages: [solidity] @@ -233,6 +235,7 @@ rules: - packages/contracts-bedrock/src/libraries/Blueprint.sol - packages/contracts-bedrock/src/dispute/lib/Errors.sol - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol @@ -331,6 +334,9 @@ rules: - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol - packages/contracts-bedrock/src/governance/MintManager.sol + - packages/contracts-bedrock/src/governance/ProposalValidator.sol + - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ProposalValidator.sol - packages/contracts-bedrock/src/periphery/TransferOnion.sol - packages/contracts-bedrock/src/periphery/faucet/Faucet.sol - packages/contracts-bedrock/src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol new file mode 100644 index 00000000000..fd9e773b466 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {VotingModule} from "src/governance/VotingModule.sol"; +interface IOptimismGovernor { + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint8 proposalType + ) external returns (uint256 proposalId); + + function proposeWithModule( + VotingModule module, + bytes memory proposalData, + string memory description, + uint8 proposalType + ) external returns (uint256 proposalId); +} \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol new file mode 100644 index 00000000000..caaf75aee42 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGovernanceToken} from "./IGovernanceToken.sol"; +import {IOptimismGovernor} from "./IOptimismGovernor.sol"; + +/// @title IProposalValidator +/// @notice Interface for the ProposalValidator contract. +interface IProposalValidator { + error ProposalValidator_InsufficientApprovals(); + error ProposalValidator_ProposalAlreadyApproved(); + error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_InsufficientVotingPower(); + error ProposalValidator_InvalidAttestation(); + + struct ProposalData { + address proposer; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalType proposalType; + bool inVoting; + mapping(address => bool) delegateApprovals; + uint256 remainingApprovalsRequired; + } + + struct ImmutableProposalTypeData { + address[] targets; + uint256[] values; + string[] signatures; + } + + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } + + event ProposalSubmitted( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalType proposalType + ); + + event ProposalApproved( + uint256 indexed proposalId, + address indexed approver + ); + + event ProposalMovedToVote( + uint256 indexed proposalId, + address indexed executor + ); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + + event VotingCycleBlockSet(uint256 newVotingCycleBlock); + + event DistributionThresholdSet(uint256 newDistributionThreshold); + + event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + function submitProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description, + ProposalType _proposalType, + bytes32 _attestationUid + ) external returns (uint256 proposalId_); + + function approveProposal(uint256 _proposalId) external; + + function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_); + + function setMinimumVotingPower(uint256 _minimumVotingPower) external; + + function setVotingCycleBlock(uint256 _votingCycleBlock) external; + + function setDistributionThreshold(uint256 _distributionThreshold) external; + + function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function renounceOwnership() external; + + function canSignOff(address _delegate) external view returns (bool canSignOff_); + + function transferOwnership(address newOwner) external; + + function minimumVotingPower() external view returns (uint256); + + function votingCycleBlock() external view returns (uint256); + + function distributionThreshold() external view returns (uint256); + + function VOTING_TOKEN() external view returns (IGovernanceToken); + + function GOVERNOR() external view returns (IOptimismGovernor); + + function owner() external view returns (address); + + function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function __constructor__( + address _owner, + IOptimismGovernor _governor, + IGovernanceToken _votingToken, + bytes32 _attestationSchemaUid, + uint256 _minimumVotingPower, + uint256 _votingCycleBlock, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals, + ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) external; +} diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json new file mode 100644 index 00000000000..8044503e6fc --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -0,0 +1,527 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "contract IOptimismGovernor", + "name": "_governor", + "type": "address" + }, + { + "internalType": "contract IGovernanceToken", + "name": "_votingToken", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_attestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + }, + { + "internalType": "enum ProposalValidator.ProposalType[]", + "name": "_proposalTypes", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "_requiredApprovals", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + } + ], + "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", + "name": "_immutableProposalTypeDatas", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GOVERNOR", + "outputs": [ + { + "internalType": "contract IOptimismGovernor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VOTING_TOKEN", + "outputs": [ + { + "internalType": "contract IGovernanceToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "approveProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_delegate", + "type": "address" + } + ], + "name": "canSignOff", + "outputs": [ + { + "internalType": "bool", + "name": "canSignOff_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "distributionThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minimumVotingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "moveToVote", + "outputs": [ + { + "internalType": "uint256", + "name": "governorProposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + } + ], + "name": "setDistributionThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + } + ], + "name": "setMinimumVotingPower", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_requiredApprovals", + "type": "uint256" + } + ], + "name": "setProposalRequiredApprovals", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + } + ], + "name": "setVotingCycleBlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_calldatas", + "type": "bytes[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + } + ], + "name": "submitProposal", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "votingCycleBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newDistributionThreshold", + "type": "uint256" + } + ], + "name": "DistributionThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newMinimumVotingPower", + "type": "uint256" + } + ], + "name": "MinimumVotingPowerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newApprovalThreshold", + "type": "uint256" + } + ], + "name": "ProposalApprovalThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ProposalApproved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "executor", + "type": "address" + } + ], + "name": "ProposalMovedToVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + }, + { + "indexed": false, + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + } + ], + "name": "ProposalSubmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newVotingCycleBlock", + "type": "uint256" + } + ], + "name": "VotingCycleBlockSet", + "type": "event" + }, + { + "inputs": [], + "name": "ProposalValidator_InsufficientApprovals", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InsufficientVotingPower", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidAttestation", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyApproved", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyInVoting", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json new file mode 100644 index 00000000000..4050cff3575 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -0,0 +1,58 @@ +[ + { + "bytes": "20", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "address" + }, + { + "bytes": "32", + "label": "minimumVotingPower", + "offset": 0, + "slot": "1", + "type": "uint256" + }, + { + "bytes": "32", + "label": "votingCycleBlock", + "offset": 0, + "slot": "2", + "type": "uint256" + }, + { + "bytes": "32", + "label": "distributionThreshold", + "offset": 0, + "slot": "3", + "type": "uint256" + }, + { + "bytes": "32", + "label": "_proposalRequiredApprovals", + "offset": 0, + "slot": "4", + "type": "mapping(enum ProposalValidator.ProposalType => uint256)" + }, + { + "bytes": "32", + "label": "_proposalTypeData", + "offset": 0, + "slot": "5", + "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" + }, + { + "bytes": "32", + "label": "_proposals", + "offset": 0, + "slot": "6", + "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" + }, + { + "bytes": "32", + "label": "_proposalCounter", + "offset": 0, + "slot": "7", + "type": "uint256" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol new file mode 100644 index 00000000000..0be3d7dbe11 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; + +/// @title ProposalValidator +/// @notice The ProposalValidator contract is responsible for validating proposals and moving +/// them to the vote phase on the Optimism Governor. +contract ProposalValidator is Ownable { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when a proposal doesn't have enough delegate approvals to move to vote. + error ProposalValidator_InsufficientApprovals(); + /// @notice Thrown when a delegate attempts to approve a proposal they've already approved. + error ProposalValidator_ProposalAlreadyApproved(); + + /// @notice Thrown when attempting to move a proposal to vote that is already in voting. + error ProposalValidator_ProposalAlreadyInVoting(); + + /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. + error ProposalValidator_InsufficientVotingPower(); + + /// @notice Thrown when an invalid attestation is provided for a proposal. + error ProposalValidator_InvalidAttestation(); + + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Data structure for storing proposal information. + /// @param proposer The address that submitted the proposal. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param calldatas Function data for proposal calls. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal from the ProposalType enum. + /// @param inVoting Whether the proposal has been moved to the voting phase. + /// @param delegateApprovals Mapping of delegate addresses to their approval status. + /// @param remainingApprovalsRequired Number of approvals still needed before being able to move for voting. + struct ProposalData { + address proposer; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalType proposalType; + bool inVoting; + mapping(address => bool) delegateApprovals; + uint256 remainingApprovalsRequired; + } + + /// @notice Data structure for storing immutable proposal type data. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param signatures Function signatures for proposal calls. + struct ImmutableProposalTypeData { + address[] targets; + uint256[] values; + string[] signatures; + } + + /*////////////////////////////////////////////////////////////// + ENUMS + //////////////////////////////////////////////////////////////*/ + + /// @notice Types of proposals that can be submitted. + /// @param ProtocolOrGovernorUpgrade Proposals for upgrading the protocol or governor. + /// @param MaintenanceUpgrade Proposals for maintenance upgrades. + /// @param CouncilMemberElections Proposals for council member elections. + /// @param GovernanceFund Proposals related to the governance fund. + /// @param CouncilBudget Proposals related to the council budget. + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new proposal is submitted to the validator contract. + /// @param proposalId The ID of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param targets Target addresses for proposal calls. + /// @param values ETH values for proposal calls. + /// @param calldatas Function data for proposal calls. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalType proposalType + ); + + /// @notice Emitted when a delegate approves a proposal. + /// @param proposalId The ID of the approved proposal. + /// @param approver The address of the delegate who approved the proposal. + event ProposalApproved(uint256 indexed proposalId, address indexed approver); + + /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. + /// @param proposalId The ID of the proposal moved to vote. + /// @param executor The address that executed the move to vote. + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); + + /// @notice Emitted when the minimum voting power is set. + /// @param newMinimumVotingPower The new minimum voting power. + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + + /// @notice Emitted when the voting cycle block is set. + /// @param newVotingCycleBlock The new voting cycle block. + event VotingCycleBlockSet(uint256 newVotingCycleBlock); + + /// @notice Emitted when the distribution threshold is set. + /// @param newDistributionThreshold The new distribution threshold. + event DistributionThresholdSet(uint256 newDistributionThreshold); + + /// @notice Emitted when the number of approvals required for a proposal type is set. + /// @param proposalType The type of proposal. + /// @param newApprovalThreshold The new approval threshold. + event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + /// @notice The schema UID for attestations in the Ethereum Attestation Service. + /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } + bytes32 public immutable ATTESTATION_SCHEMA_UID; + + /// @notice The Optimism Governor contract that will handle the voting phase. + IOptimismGovernor public immutable GOVERNOR; + + /// @notice The token used to determine voting power. + IGovernanceToken public immutable VOTING_TOKEN; + + /// @notice The minimum voting power required for a delegate to approve proposals. + uint256 public minimumVotingPower; + + /// @notice The block number of the current voting cycle. + uint256 public votingCycleBlock; + + /// @notice The max amount of tokens that can be distributed in a proposal. + uint256 public distributionThreshold; + + /// @notice The number of approvals required for each proposal type. + mapping(ProposalType => uint256) private _proposalRequiredApprovals; + + /// @notice The immutable data for each proposal type. + mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; + + /// @notice Mapping of proposal IDs to their corresponding proposal data. + mapping(uint256 => ProposalData) private _proposals; + + /// @notice Counter for generating unique proposal IDs. + uint256 private _proposalCounter; + + /// @notice Initializes the ProposalValidator contract. + /// @param _owner The address that will own the contract. + /// @param _governor The Optimism Governor contract address. + /// @param _votingToken The token used to determine voting power. + /// @param _attestationSchemaUid The schema UID for attestations in EAS. + /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. + /// @param _votingCycleBlock The block number of the current voting cycle. + /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. + /// @param _proposalTypes Array of proposal types to set approval thresholds for. + /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. + /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. + constructor( + address _owner, + IOptimismGovernor _governor, + IGovernanceToken _votingToken, + bytes32 _attestationSchemaUid, + uint256 _minimumVotingPower, + uint256 _votingCycleBlock, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals, + ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) { + transferOwnership(_owner); + GOVERNOR = _governor; + VOTING_TOKEN = _votingToken; + ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + + _setMinimumVotingPower(_minimumVotingPower); + _setVotingCycleBlock(_votingCycleBlock); + _setDistributionThreshold(_distributionThreshold); + + for (uint256 i = 0; i < _proposalTypes.length; i++) { + _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); + _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; + } + } + + /// @notice Submit a proposal for delegate approval. + /// @param _targets Target addresses for proposal calls. + /// @param _values ETH values for proposal calls. + /// @param _calldatas Function data for proposal calls. + /// @param _description Description of the proposal. + /// @param _proposalType Type of the proposal. + /// @param _attestationUid The UID of the attestation proving eligibility. + /// @return proposalId_ The ID of the submitted proposal. + function submitProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description, + ProposalType _proposalType, + bytes32 _attestationUid + ) + external + returns (uint256 proposalId_) + { + _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); + + proposalId_ = ++_proposalCounter; + + ProposalData storage proposal = _proposals[proposalId_]; + proposal.proposer = msg.sender; + proposal.targets = _targets; + proposal.values = _values; + proposal.calldatas = _calldatas; + proposal.description = _description; + proposal.proposalType = _proposalType; + proposal.inVoting = false; + proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes + + emit ProposalSubmitted(proposalId_, msg.sender, _targets, _values, _calldatas, _description, _proposalType); + + return proposalId_; + } + + /// @notice Approve a proposal (only callable by delegates with sufficient voting power). + /// @param _proposalId The ID of the proposal to approve. + function approveProposal(uint256 _proposalId) external { + if (!canSignOff(msg.sender)) { + revert ProposalValidator_InsufficientVotingPower(); + } + + ProposalData storage proposal = _proposals[_proposalId]; + + if (proposal.delegateApprovals[msg.sender]) { + revert ProposalValidator_ProposalAlreadyApproved(); + } + + proposal.delegateApprovals[msg.sender] = true; + proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted + + emit ProposalApproved(_proposalId, msg.sender); + } + + /// @notice Move a proposal to voting phase after sufficient delegate approvals. + /// @dev After passing all checks, the proposal is submitted with a external call to the governor contract. + /// @param _proposalId The ID of the proposal to move to vote. + /// @return governorProposalId_ The proposal ID in the governor contract. + function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_) { + ProposalData storage proposal = _proposals[_proposalId]; + + if (proposal.remainingApprovalsRequired > 0) { + revert ProposalValidator_InsufficientApprovals(); + } + + if (proposal.inVoting) { + revert ProposalValidator_ProposalAlreadyInVoting(); + } + + proposal.inVoting = true; + + governorProposalId_ = GOVERNOR.propose( + proposal.targets, proposal.values, proposal.calldatas, proposal.description, uint8(proposal.proposalType) + ); + + emit ProposalMovedToVote(_proposalId, msg.sender); + + return governorProposalId_; + } + + /// @notice Returns whether a delegate has enough voting power to approve a proposal. + /// @param _delegate The address of the delegate to check. + /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. + function canSignOff(address _delegate) public view returns (bool canSignOff_) { + return VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; + } + + /// @notice Sets the minimum voting power required for a delegate to approve proposals. + /// @param _minimumVotingPower The new minimum voting power threshold. + function setMinimumVotingPower(uint256 _minimumVotingPower) external onlyOwner { + _setMinimumVotingPower(_minimumVotingPower); + } + + /// @notice Sets the block number of the current voting cycle. + /// @param _votingCycleBlock The new voting cycle block number. + function setVotingCycleBlock(uint256 _votingCycleBlock) external onlyOwner { + _setVotingCycleBlock(_votingCycleBlock); + } + + /// @notice Sets the max amount of tokens that can be distributed in a proposal. + /// @param _distributionThreshold The new distribution threshold. + function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { + _setDistributionThreshold(_distributionThreshold); + } + + /// @notice Sets the number of approvals required for each proposal type. + /// @param _proposalType The type of proposal to set the required approvals for. + /// @param _requiredApprovals The new required approvals. + function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external onlyOwner { + _setProposalRequiredApprovals(_proposalType, _requiredApprovals); + } + + /// @notice Validates a proposal before submission. + /// @dev Checks if the proposal requires approval and validates the attestation. + /// @param _targets Target addresses for proposal calls. + /// @param _values ETH values for proposal calls. + /// @param _calldatas Function data for proposal calls. + /// @param _proposalType Type of the proposal. + /// @param _attestationUid The UID of the attestation proving eligibility. + function _validateProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + ProposalType _proposalType, + bytes32 _attestationUid + ) + private + view + { + if (_requiresAttestation(_proposalType)) { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + if ( + attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + || !_isValidAttestationData(attestation.data, _proposalType) + ) { + revert ProposalValidator_InvalidAttestation(); + } + } + } + + /// @notice Determines if a proposal type requires approval via attestation. + /// @param _proposalType The type of proposal to check. + /// @return requiresAttestation_ True if the proposal type requires approval, false otherwise. + function _requiresAttestation(ProposalType _proposalType) private pure returns (bool requiresAttestation_) { + return _proposalType == ProposalType.ProtocolOrGovernorUpgrade + || _proposalType == ProposalType.MaintenanceUpgrade || _proposalType == ProposalType.CouncilMemberElections; + } + + /// @notice Validates the attestation data for a proposal. + /// @dev Checks that the sender is the approved delegate and that the proposal type is correct. + /// @param _data The attestation data to validate. + /// @param _expectedProposalType The expected proposal type from the attestation. + /// @return isValid_ True if the attestation data is valid, false otherwise. + function _isValidAttestationData( + bytes memory _data, + ProposalType _expectedProposalType + ) + private + view + returns (bool isValid_) + { + (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); + return approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + } + + /// @notice Private function to set the minimum voting power and emit event. + /// @param _minimumVotingPower The new minimum voting power threshold. + function _setMinimumVotingPower(uint256 _minimumVotingPower) private { + minimumVotingPower = _minimumVotingPower; + emit MinimumVotingPowerSet(_minimumVotingPower); + } + + /// @notice Private function to set the voting cycle block and emit event. + /// @param _votingCycleBlock The new voting cycle block number. + function _setVotingCycleBlock(uint256 _votingCycleBlock) private { + votingCycleBlock = _votingCycleBlock; + emit VotingCycleBlockSet(_votingCycleBlock); + } + + /// @notice Private function to set the distribution threshold and emit event. + /// @param _distributionThreshold The new distribution threshold. + function _setDistributionThreshold(uint256 _distributionThreshold) private { + distributionThreshold = _distributionThreshold; + emit DistributionThresholdSet(_distributionThreshold); + } + + /// @notice Private function to set a proposal's type required approvals and emit event. + /// @param _proposalType The type of proposal to set the required approvals for. + /// @param _requiredApprovals The new required approvals. + function _setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) private { + _proposalRequiredApprovals[_proposalType] = _requiredApprovals; + emit ProposalApprovalThresholdSet(_proposalType, _requiredApprovals); + } +} diff --git a/packages/contracts-bedrock/src/governance/VotingModule.sol b/packages/contracts-bedrock/src/governance/VotingModule.sol new file mode 100644 index 00000000000..02b692d87f1 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/VotingModule.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +abstract contract VotingModule { + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + address immutable governor; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotGovernor(); // nosemgrep: + error ExistingProposal(); // nosemgrep: + error InvalidParams(); // nosemgrep: + error AlreadyVoted(); // nosemgrep: + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + function _onlyGovernor() internal view { + if (msg.sender != governor) revert NotGovernor(); + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) { + governor = _governor; + } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external virtual; + + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) + external + virtual; + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _formatExecuteParams( + uint256 proposalId, + bytes memory proposalData + ) + external + virtual + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas); + + function _voteSucceeded(uint256 /* proposalId */ ) external view virtual returns (bool) { + return true; + } + + function COUNTING_MODE() external pure virtual returns (string memory); + + function PROPOSAL_DATA_ENCODING() external pure virtual returns (string memory); + + function VOTE_PARAMS_ENCODING() external pure virtual returns (string memory); +} diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol new file mode 100644 index 00000000000..7e9c85a18f0 --- /dev/null +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Interfaces +import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; +import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; + +// Contracts +import { ProposalValidator } from "src/governance/ProposalValidator.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +/// @title ProposalValidator_Init +/// @notice Setup contract for ProposalValidator tests +contract ProposalValidator_Init is CommonTest { + uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP + uint256 public constant VOTING_CYCLE_BLOCK = 100; + uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; + uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; + uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + + address owner; + address rando; + address topDelegate_A; + address topDelegate_B; + address topDelegate_C; + address topDelegate_D; + + ProposalValidator public validator; + IOptimismGovernor public governor; + bytes32 public ATTESTATION_SCHEMA_UID; + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + /// @notice Helper function to make a top delegate. + function _makeTopDelegate(string memory _name) internal returns (address) { + address delegate = makeAddr(_name); + deal(address(governanceToken), delegate, TOP_DELEGATE_VOTING_POWER); + vm.prank(delegate); + governanceToken.delegate(delegate); + return delegate; + } + + /// @notice Helper function to make a (top) delegate approve a proposal. + function _approveProposal(address _delegate, uint256 _proposalId) internal { + vm.prank(_delegate); + validator.approveProposal(_proposalId); + } + + function _getProposalTypesRequiredApprovalsAndImmutableData() + internal + pure + returns ( + ProposalValidator.ProposalType[] memory, + uint256[] memory, + ProposalValidator.ImmutableProposalTypeData[] memory + ) + { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + proposalTypes[3] = ProposalValidator.ProposalType.GovernanceFund; + proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; + + uint256[] memory requiredApprovals = new uint256[](5); + requiredApprovals[0] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[1] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[2] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; + requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; + + ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData = + new ProposalValidator.ImmutableProposalTypeData[](5); + immutableProposalTypeData[0] = ProposalValidator.ImmutableProposalTypeData({ + targets: new address[](1), + values: new uint256[](1), + signatures: new string[](1) + }); + + return (proposalTypes, requiredApprovals, immutableProposalTypeData); + } + + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + owner = governanceToken.owner(); + rando = makeAddr("rando"); + governor = IOptimismGovernor(makeAddr("governor")); + + vm.prank(owner); + ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + ); + + ( + ProposalValidator.ProposalType[] memory proposalTypes, + uint256[] memory requiredApprovals, + ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData + ) = _getProposalTypesRequiredApprovalsAndImmutableData(); + + validator = new ProposalValidator( + owner, + governor, + governanceToken, + ATTESTATION_SCHEMA_UID, + MINIMUM_VOTING_POWER, + VOTING_CYCLE_BLOCK, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals, + immutableProposalTypeData + ); + + topDelegate_A = _makeTopDelegate("topDelegate_A"); + topDelegate_B = _makeTopDelegate("topDelegate_B"); + topDelegate_C = _makeTopDelegate("topDelegate_C"); + topDelegate_D = _makeTopDelegate("topDelegate_D"); + } + + /// @notice Helper to create a valid attestation for a proposal + function _createAttestation( + address _delegate, + ProposalValidator.ProposalType _proposalType + ) + internal + returns (bytes32) + { + vm.prank(owner); + return IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(_delegate, _proposalType), + value: 0 + }) + }) + ); + } + + /// @notice Helper to create a standard proposal setup + function _createProposalSetup() + internal + view + returns ( + address[] memory targets_, + uint256[] memory values_, + bytes[] memory calldatas_, + string memory description_ + ) + { + targets_ = new address[](1); + targets_[0] = address(0); + values_ = new uint256[](1); + values_[0] = 0; + calldatas_ = new bytes[](1); + calldatas_[0] = bytes(""); + description_ = "Test proposal"; + } +} + +/// @title ProposalValidator_SubmitProposal_Test +/// @notice Happy path tests for submitProposal function +contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { + function test_submitProposal_succeeds() public { + (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + uint256 proposalId = + validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); + + assertEq(proposalId, 1); + } +} + +/// @title ProposalValidator_SubmitProposal_TestFail +/// @notice Sad path tests for submitProposal function +contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { + function test_submitProposal_invalidAttestation_reverts() public { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID + + vm.prank(topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); + } + + function test_submitProposal_wrongAttester_reverts() public { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation with wrong delegate + bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); + + vm.prank(topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } +} + +/// @title ProposalValidator_ApproveProposal_Test +/// @notice Happy path tests for approveProposal function +contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + uint256 proposalId; + + function setUp() public override { + super.setUp(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_approveProposal_succeeds() public { + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + } +} + +/// @title ProposalValidator_ApproveProposal_TestFail +/// @notice Sad path tests for approveProposal function +contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { + uint256 proposalId; + + function setUp() public override { + super.setUp(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_approveProposal_insufficientVotingPower_reverts() public { + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); + _approveProposal(rando, proposalId); + } + + function test_approveProposal_alreadyApproved_reverts() public { + _approveProposal(topDelegate_A, proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); + _approveProposal(topDelegate_A, proposalId); + } +} + +/// @title ProposalValidator_MoveToVote_Test +/// @notice Happy path tests for moveToVote function +contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { + uint256 proposalId; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalValidator.ProposalType proposalType; + + function setUp() public override { + super.setUp(); + + (targets, values, calldatas, description) = _createProposalSetup(); + + proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + } + + function test_moveToVote_succeeds() public { + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + uint256 governorProposalId = validator.moveToVote(proposalId); + + assertEq(governorProposalId, 1); + } +} + +/// @title ProposalValidator_MoveToVote_TestFail +/// @notice Sad path tests for moveToVote function +contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { + uint256 proposalId; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + ProposalValidator.ProposalType proposalType; + + function setUp() public override { + super.setUp(); + + (targets, values, calldatas, description) = _createProposalSetup(); + + proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + } + + function test_moveToVote_insufficientApprovals_reverts() public { + // Only approve with 3 delegates (need 4) + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } + + function test_moveToVote_alreadyProposed_reverts() public { + // Approve with all 4 delegates + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_D, proposalId); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + validator.moveToVote(proposalId); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } +} + +/// @title ProposalValidator_Getters_Test +/// @notice Tests for getter functions +contract ProposalValidator_Getters_Test is ProposalValidator_Init { + function test_canSignOff_succeeds() public { + bool canSignOff = validator.canSignOff(topDelegate_A); + assertTrue(canSignOff); + + bool cannotSignOff = validator.canSignOff(rando); + assertFalse(cannotSignOff); + } +} + +/// @title ProposalValidator_Setters_Test +/// @notice Tests for setter functions +contract ProposalValidator_Setters_Test is ProposalValidator_Init { +// TODO: Implement tests for setters +} + +/// @title ProposalValidator_Integration_Test +/// @notice Integration tests for the full proposal flow +contract ProposalValidator_Integration_Test is ProposalValidator_Init { + function test_proposalFullFlow_succeeds() public { + // Create a proposal + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = + _createProposalSetup(); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + + vm.prank(topDelegate_A); + uint256 proposalId = + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + + assertEq(proposalId, 1); + + // It reverts when caller is not a top delegate + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); + _approveProposal(rando, proposalId); + + _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_B, proposalId); + _approveProposal(topDelegate_C, proposalId); + + // It reverts when proposal hasn't reached the required approvals + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + + _approveProposal(topDelegate_D, proposalId); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encode(1) + ); + + vm.prank(owner); + validator.moveToVote(proposalId); + + // It reverts when proposal is already in voting phase + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.prank(owner); + validator.moveToVote(proposalId); + } +} From 9d309acebc8a0e3b5dbf3362b3ce8ca03c089d17 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 6 May 2025 14:47:08 -0300 Subject: [PATCH 36/73] feat: duplicated proposals check (#378) * feat: add initial interface and logic * refactor: remove installed governor submodule * chore: remove xERC20 * feat: add proposal routing full flow * feat: check voting power and required proposals * refactor: rename to ProposalValidator * feat: add EAS validation for certain Proposal Types * feat: add duplicated proposals validation * chore: fix attestation schema approved address naming Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * chore: remove management functions * chore: run pre-pr * refacto: follow style guide for function parameters and return variables * docs: add natspec, remove unused errors * chore: remove management functions from interface * chore: make voting token immutable * perf: make governor immutable * feat: add validator management functions * chore: add comments for imports in ProposalValidator * test: add unit tests * chore: run pre-pr * fix: semgrep warnings * chore: rename MaintenanceUpgradeProposals --> MaintenanceUpgrade * chore(semgrep): add excluded governance files * chore: fix coding style * chore: add ImmutableProposalTypeData * chore: improve errors naming * docs: improve natspec Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * docs: add technical explanation on attestation validation function * feat: add _proposalTypeData mapping * chore: keep private functions consistency * chore: improve required attestation naming * chore: run pre-pr * chore: more descriptive errors * chore: confusing error name in submitProposal --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 29 ++-- .../snapshots/abi/ProposalValidator.json | 69 +++++--- .../storageLayout/ProposalValidator.json | 9 +- .../src/governance/ProposalValidator.sol | 152 +++++++++++------- .../test/governance/ProposalValidator.t.sol | 145 +++++++++-------- 5 files changed, 241 insertions(+), 163 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index caaf75aee42..4dc0cd45ad1 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -9,17 +9,15 @@ import {IOptimismGovernor} from "./IOptimismGovernor.sol"; interface IProposalValidator { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); - error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_ProposalAlreadySubmitted(); error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); + error ProposalValidator_ProposalDoesNotExist(); struct ProposalData { address proposer; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; ProposalType proposalType; + uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; @@ -40,22 +38,23 @@ interface IProposalValidator { } event ProposalSubmitted( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed proposer, address[] targets, uint256[] values, bytes[] calldatas, string description, - ProposalType proposalType + ProposalType proposalType, + uint8 proposalTypeConfigurator ); event ProposalApproved( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed approver ); event ProposalMovedToVote( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed executor ); @@ -75,12 +74,18 @@ interface IProposalValidator { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, + uint8 _proposalTypeConfigurator, bytes32 _attestationUid - ) external returns (uint256 proposalId_); + ) external returns (bytes32 proposalHash_); - function approveProposal(uint256 _proposalId) external; + function approveProposal(bytes32 _proposalHash) external; - function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_); + function moveToVote( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) external returns (uint256 governorProposalId_); function setMinimumVotingPower(uint256 _minimumVotingPower) external; diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 8044503e6fc..bdd01c8d1d9 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -114,9 +114,9 @@ { "inputs": [ { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "_proposalHash", + "type": "bytes32" } ], "name": "approveProposal", @@ -172,9 +172,24 @@ { "inputs": [ { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" + "internalType": "address[]", + "name": "_targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_calldatas", + "type": "bytes[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" } ], "name": "moveToVote", @@ -292,6 +307,11 @@ "name": "_proposalType", "type": "uint8" }, + { + "internalType": "uint8", + "name": "_proposalTypeConfigurator", + "type": "uint8" + }, { "internalType": "bytes32", "name": "_attestationUid", @@ -301,9 +321,9 @@ "name": "submitProposal", "outputs": [ { - "internalType": "uint256", - "name": "proposalId_", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" } ], "stateMutability": "nonpayable", @@ -404,9 +424,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -423,9 +443,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -442,9 +462,9 @@ "inputs": [ { "indexed": true, - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" }, { "indexed": true, @@ -481,6 +501,12 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "proposalType", "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], "name": "ProposalSubmitted", @@ -521,7 +547,12 @@ }, { "inputs": [], - "name": "ProposalValidator_ProposalAlreadyInVoting", + "name": "ProposalValidator_ProposalAlreadySubmitted", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 4050cff3575..f7fdeefae8c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -46,13 +46,6 @@ "label": "_proposals", "offset": 0, "slot": "6", - "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" - }, - { - "bytes": "32", - "label": "_proposalCounter", - "offset": 0, - "slot": "7", - "type": "uint256" + "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 0be3d7dbe11..6661bc17030 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -22,11 +22,12 @@ contract ProposalValidator is Ownable { /// @notice Thrown when a proposal doesn't have enough delegate approvals to move to vote. error ProposalValidator_InsufficientApprovals(); + /// @notice Thrown when a delegate attempts to approve a proposal they've already approved. error ProposalValidator_ProposalAlreadyApproved(); /// @notice Thrown when attempting to move a proposal to vote that is already in voting. - error ProposalValidator_ProposalAlreadyInVoting(); + error ProposalValidator_ProposalAlreadySubmitted(); /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. error ProposalValidator_InsufficientVotingPower(); @@ -34,27 +35,24 @@ contract ProposalValidator is Ownable { /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); + /// @notice Thrown when a proposal does not exist. + error ProposalValidator_ProposalDoesNotExist(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ /// @notice Data structure for storing proposal information. /// @param proposer The address that submitted the proposal. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param calldatas Function data for proposal calls. - /// @param description Description of the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. + /// @param proposalTypeConfigurator Configuration value specific to the proposal type. /// @param inVoting Whether the proposal has been moved to the voting phase. /// @param delegateApprovals Mapping of delegate addresses to their approval status. - /// @param remainingApprovalsRequired Number of approvals still needed before being able to move for voting. + /// @param remainingApprovalsRequired Number of approvals still needed before voting. struct ProposalData { address proposer; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; ProposalType proposalType; + uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; @@ -93,32 +91,34 @@ contract ProposalValidator is Ownable { //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a new proposal is submitted to the validator contract. - /// @param proposalId The ID of the submitted proposal. + /// @param proposalHash The hash of the submitted proposal. /// @param proposer The address that submitted the proposal. /// @param targets Target addresses for proposal calls. /// @param values ETH values for proposal calls. /// @param calldatas Function data for proposal calls. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. + /// @param proposalTypeConfigurator Configuration value specific to the proposal type. event ProposalSubmitted( - uint256 indexed proposalId, + bytes32 indexed proposalHash, address indexed proposer, address[] targets, uint256[] values, bytes[] calldatas, string description, - ProposalType proposalType + ProposalType proposalType, + uint8 proposalTypeConfigurator ); /// @notice Emitted when a delegate approves a proposal. - /// @param proposalId The ID of the approved proposal. + /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(uint256 indexed proposalId, address indexed approver); + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalId The ID of the proposal moved to vote. + /// @param proposalHash The hash of the proposal moved to vote. /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); /// @notice Emitted when the minimum voting power is set. /// @param newMinimumVotingPower The new minimum voting power. @@ -162,11 +162,8 @@ contract ProposalValidator is Ownable { /// @notice The immutable data for each proposal type. mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; - /// @notice Mapping of proposal IDs to their corresponding proposal data. - mapping(uint256 => ProposalData) private _proposals; - - /// @notice Counter for generating unique proposal IDs. - uint256 private _proposalCounter; + /// @notice Mapping of proposal hash to their corresponding proposal data. + mapping(bytes32 => ProposalData) private _proposals; /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. @@ -206,52 +203,60 @@ contract ProposalValidator is Ownable { } } - /// @notice Submit a proposal for delegate approval. - /// @param _targets Target addresses for proposal calls. - /// @param _values ETH values for proposal calls. - /// @param _calldatas Function data for proposal calls. - /// @param _description Description of the proposal. - /// @param _proposalType Type of the proposal. - /// @param _attestationUid The UID of the attestation proving eligibility. - /// @return proposalId_ The ID of the submitted proposal. + /// @notice Submit a proposal for delegate approval + /// @param _targets Target addresses for proposal calls + /// @param _values ETH values for proposal calls + /// @param _calldatas Function data for proposal calls + /// @param _description Description of the proposal + /// @param _proposalType Type of the proposal + /// @return proposalHash_ The hash of the submitted proposal function submitProposal( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, + uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external - returns (uint256 proposalId_) + returns (bytes32 proposalHash_) { _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - proposalId_ = ++_proposalCounter; + proposalHash_ = _hashProposal(_targets, _values, _calldatas, _description); + ProposalData storage proposal = _proposals[proposalHash_]; + + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } - ProposalData storage proposal = _proposals[proposalId_]; proposal.proposer = msg.sender; - proposal.targets = _targets; - proposal.values = _values; - proposal.calldatas = _calldatas; - proposal.description = _description; proposal.proposalType = _proposalType; + proposal.proposalTypeConfigurator = _proposalTypeConfigurator; proposal.inVoting = false; proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes - emit ProposalSubmitted(proposalId_, msg.sender, _targets, _values, _calldatas, _description, _proposalType); - - return proposalId_; + emit ProposalSubmitted( + proposalHash_, + msg.sender, + _targets, + _values, + _calldatas, + _description, + _proposalType, + _proposalTypeConfigurator + ); } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power). - /// @param _proposalId The ID of the proposal to approve. - function approveProposal(uint256 _proposalId) external { + /// @notice Approve a proposal (only callable by delegates with sufficient voting power) + /// @param _proposalHash The hash of the proposal to approve + function approveProposal(bytes32 _proposalHash) external { if (!canSignOff(msg.sender)) { revert ProposalValidator_InsufficientVotingPower(); } - ProposalData storage proposal = _proposals[_proposalId]; + ProposalData storage proposal = _proposals[_proposalHash]; if (proposal.delegateApprovals[msg.sender]) { revert ProposalValidator_ProposalAlreadyApproved(); @@ -260,40 +265,54 @@ contract ProposalValidator is Ownable { proposal.delegateApprovals[msg.sender] = true; proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted - emit ProposalApproved(_proposalId, msg.sender); + emit ProposalApproved(_proposalHash, msg.sender); } - /// @notice Move a proposal to voting phase after sufficient delegate approvals. - /// @dev After passing all checks, the proposal is submitted with a external call to the governor contract. - /// @param _proposalId The ID of the proposal to move to vote. - /// @return governorProposalId_ The proposal ID in the governor contract. - function moveToVote(uint256 _proposalId) external returns (uint256 governorProposalId_) { - ProposalData storage proposal = _proposals[_proposalId]; + /// @notice Move a proposal to voting phase after sufficient delegate approvals + /// @param _targets Target addresses for proposal calls + /// @param _values ETH values for proposal calls + /// @param _calldatas Function data for proposal calls + /// @param _description Description of the proposal + /// @return governorProposalId_ The proposal ID in the governor contract + function moveToVote( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + external + returns (uint256 governorProposalId_) + { + // Verify that the provided data matches the proposalHash + bytes32 _proposalHash = _hashProposal(_targets, _values, _calldatas, _description); + + ProposalData storage proposal = _proposals[_proposalHash]; + + if (proposal.proposer == address(0)) { + revert ProposalValidator_ProposalDoesNotExist(); + } if (proposal.remainingApprovalsRequired > 0) { revert ProposalValidator_InsufficientApprovals(); } if (proposal.inVoting) { - revert ProposalValidator_ProposalAlreadyInVoting(); + revert ProposalValidator_ProposalAlreadySubmitted(); } proposal.inVoting = true; - governorProposalId_ = GOVERNOR.propose( - proposal.targets, proposal.values, proposal.calldatas, proposal.description, uint8(proposal.proposalType) - ); - - emit ProposalMovedToVote(_proposalId, msg.sender); + governorProposalId_ = + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposal.proposalTypeConfigurator); - return governorProposalId_; + emit ProposalMovedToVote(_proposalHash, msg.sender); } /// @notice Returns whether a delegate has enough voting power to approve a proposal. /// @param _delegate The address of the delegate to check. /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. function canSignOff(address _delegate) public view returns (bool canSignOff_) { - return VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; + canSignOff_ = VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; } /// @notice Sets the minimum voting power required for a delegate to approve proposals. @@ -371,7 +390,20 @@ contract ProposalValidator is Ownable { returns (bool isValid_) { (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); - return approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + } + + function _hashProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + internal + pure + returns (bytes32 proposalHash_) + { + return keccak256(abi.encode(_targets, _values, _calldatas, _description)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7e9c85a18f0..b58c5c7995b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -35,6 +35,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator public validator; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; + bytes32 public proposalHash; /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -52,9 +53,9 @@ contract ProposalValidator_Init is CommonTest { } /// @notice Helper function to make a (top) delegate approve a proposal. - function _approveProposal(address _delegate, uint256 _proposalId) internal { + function _approveProposal(address _delegate, bytes32 _proposalHash) internal { vm.prank(_delegate); - validator.approveProposal(_proposalId); + validator.approveProposal(_proposalHash); } function _getProposalTypesRequiredApprovalsAndImmutableData() @@ -181,13 +182,16 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Submit the proposal vm.prank(topDelegate_A); - uint256 proposalId = - validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); + bytes32 proposalHash = validator.submitProposal( + _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid + ); - assertEq(proposalId, 1); + assertEq(proposalHash, keccak256(abi.encode(_targets, _values, _calldatas, _description))); } } @@ -199,11 +203,14 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); + validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, invalidAttestationUid + ); } function test_submitProposal_wrongAttester_reverts() public { @@ -211,21 +218,22 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; // Create attestation with wrong delegate bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } } /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - uint256 proposalId; - function setUp() public override { super.setUp(); @@ -233,25 +241,26 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_approveProposal_succeeds() public { - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); } } /// @title ProposalValidator_ApproveProposal_TestFail /// @notice Sad path tests for approveProposal function contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { - uint256 proposalId; - function setUp() public override { super.setUp(); @@ -259,34 +268,37 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_approveProposal_insufficientVotingPower_reverts() public { vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalId); + _approveProposal(rando, proposalHash); } function test_approveProposal_alreadyApproved_reverts() public { - _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_A, proposalHash); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - _approveProposal(topDelegate_A, proposalId); + _approveProposal(topDelegate_A, proposalHash); } } /// @title ProposalValidator_MoveToVote_Test /// @notice Happy path tests for moveToVote function contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { - uint256 proposalId; address[] targets; uint256[] values; bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; + uint8 proposalTypeConfigurator; function setUp() public override { super.setUp(); @@ -294,26 +306,31 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); } function test_moveToVote_succeeds() public { _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(proposalId); + uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); assertEq(governorProposalId, 1); } @@ -322,12 +339,12 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { /// @title ProposalValidator_MoveToVote_TestFail /// @notice Sad path tests for moveToVote function contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { - uint256 proposalId; address[] targets; uint256[] values; bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; + uint8 proposalTypeConfigurator; function setUp() public override { super.setUp(); @@ -335,42 +352,47 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalId = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); } function test_moveToVote_insufficientApprovals_reverts() public { // Only approve with 3 delegates (need 4) - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } function test_moveToVote_alreadyProposed_reverts() public { // Approve with all 4 delegates - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - _approveProposal(topDelegate_D, proposalId); + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } } @@ -401,41 +423,36 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - uint256 proposalId = - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - - assertEq(proposalId, 1); - - // It reverts when caller is not a top delegate - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalId); - - _approveProposal(topDelegate_A, proposalId); - _approveProposal(topDelegate_B, proposalId); - _approveProposal(topDelegate_C, proposalId); - - // It reverts when proposal hasn't reached the required approvals - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(owner); - validator.moveToVote(proposalId); + bytes32 proposalHash = validator.submitProposal( + targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid + ); - _approveProposal(topDelegate_D, proposalId); + // Collect all required approvals + _approveProposal(topDelegate_A, proposalHash); + _approveProposal(topDelegate_B, proposalHash); + _approveProposal(topDelegate_C, proposalHash); + _approveProposal(topDelegate_D, proposalHash); + // Mock the governor call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, uint8(proposalType))), + abi.encodeCall( + IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) + ), abi.encode(1) ); + // Move to vote phase vm.prank(owner); - validator.moveToVote(proposalId); + uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); // It reverts when proposal is already in voting phase - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyInVoting.selector); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); vm.prank(owner); - validator.moveToVote(proposalId); + validator.moveToVote(targets, values, calldatas, description); } } From de3851c204fea8b9ce9f1389c01b32ec1955c16a Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 23 May 2025 11:27:28 -0300 Subject: [PATCH 37/73] feat: add upgradeability to ProposalValidator contract (#384) * feat: add upgradeability to ProposalValidator contract * chore: fix styling * docs: use correct natspec in ProosalValidator contract * feat: add semver * feat: add reinitializable base --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 30 +- .../snapshots/abi/ApprovalVotingModule.json | 346 ++++++++++++++++++ .../snapshots/abi/ProposalValidator.json | 162 +++++--- .../snapshots/semver-lock.json | 4 + .../storageLayout/ApprovalVotingModule.json | 16 + .../storageLayout/ProposalValidator.json | 42 ++- .../src/governance/ProposalValidator.sol | 51 ++- .../test/governance/ProposalValidator.t.sol | 33 +- 8 files changed, 586 insertions(+), 98 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 4dc0cd45ad1..0034069fc25 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IGovernanceToken} from "./IGovernanceToken.sol"; -import {IOptimismGovernor} from "./IOptimismGovernor.sol"; +// Interfaces +import {IGovernanceToken} from './IGovernanceToken.sol'; +import {IOptimismGovernor} from './IOptimismGovernor.sol'; +import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. -interface IProposalValidator { +interface IProposalValidator is ISemver { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); + error ReinitializableBase_ZeroInitVersion(); struct ProposalData { address proposer; @@ -68,6 +71,8 @@ interface IProposalValidator { event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event Initialized(uint8 version); + function submitProposal( address[] memory _targets, uint256[] memory _values, @@ -113,18 +118,23 @@ interface IProposalValidator { function owner() external view returns (address); + function initVersion() external view returns (uint8); + function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - - function __constructor__( + + function initialize( address _owner, - IOptimismGovernor _governor, - IGovernanceToken _votingToken, - bytes32 _attestationSchemaUid, uint256 _minimumVotingPower, uint256 _votingCycleBlock, uint256 _distributionThreshold, - ProposalType[] memory _proposalTypes, + IProposalValidator.ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals, - ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + IProposalValidator.ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + ) external; + + function __constructor__( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _votingToken ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json new file mode 100644 index 00000000000..d531b81bb48 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json @@ -0,0 +1,346 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_governor", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "COUNTING_MODE", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "PROPOSAL_DATA_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "VOTE_PARAMS_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "budgetTokensSpent", + "type": "uint256" + } + ], + "name": "_afterExecute", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint8", + "name": "support", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "_countVote", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + } + ], + "name": "_formatExecuteParams", + "outputs": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "calldatas", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "_voteSucceeded", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountTotalVotes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountVotes", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "proposals", + "outputs": [ + { + "internalType": "address", + "name": "governor", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initBalance", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "maxApprovals", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "criteria", + "type": "uint8" + }, + { + "internalType": "address", + "name": "budgetToken", + "type": "address" + }, + { + "internalType": "uint128", + "name": "criteriaValue", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "budgetAmount", + "type": "uint128" + } + ], + "internalType": "struct ProposalSettings", + "name": "settings", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proposalData", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "descriptionHash", + "type": "bytes32" + } + ], + "name": "propose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "BudgetExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "ExistingProposal", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParams", + "type": "error" + }, + { + "inputs": [], + "name": "MaxApprovalsExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxChoicesExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernor", + "type": "error" + }, + { + "inputs": [], + "name": "OptionsNotStrictlyAscending", + "type": "error" + }, + { + "inputs": [], + "name": "WrongProposalId", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index bdd01c8d1d9..0430ce901f4 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -2,9 +2,9 @@ { "inputs": [ { - "internalType": "address", - "name": "_owner", - "type": "address" + "internalType": "bytes32", + "name": "_attestationSchemaUid", + "type": "bytes32" }, { "internalType": "contract IOptimismGovernor", @@ -15,58 +15,6 @@ "internalType": "contract IGovernanceToken", "name": "_votingToken", "type": "address" - }, - { - "internalType": "bytes32", - "name": "_attestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_distributionThreshold", - "type": "uint256" - }, - { - "internalType": "enum ProposalValidator.ProposalType[]", - "name": "_proposalTypes", - "type": "uint8[]" - }, - { - "internalType": "uint256[]", - "name": "_requiredApprovals", - "type": "uint256[]" - }, - { - "components": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "string[]", - "name": "signatures", - "type": "string[]" - } - ], - "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", - "name": "_immutableProposalTypeDatas", - "type": "tuple[]" } ], "stateMutability": "nonpayable", @@ -156,6 +104,79 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minimumVotingPower", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_distributionThreshold", + "type": "uint256" + }, + { + "internalType": "enum ProposalValidator.ProposalType[]", + "name": "_proposalTypes", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "_requiredApprovals", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "targets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "string[]", + "name": "signatures", + "type": "string[]" + } + ], + "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", + "name": "_immutableProposalTypeDatas", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "minimumVotingPower", @@ -342,6 +363,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "votingCycleBlock", @@ -368,6 +402,19 @@ "name": "DistributionThresholdSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -554,5 +601,10 @@ "inputs": [], "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index e7faca31901..5132c553bc3 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -171,6 +171,10 @@ "initCodeHash": "0x9c954076097eb80f70333a387f12ba190eb9374aebb923ce30ecfe1d17030cc0", "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, + "src/governance/ProposalValidator.sol:ProposalValidator": { + "initCodeHash": "0x79a40d1aa2eca36a8a8bcacfed58e42b6eca856e2e12898433a88d8aeaa6e74d", + "sourceCodeHash": "0x0049245cc58386fd48e72280d4d629d520c6c21ab061379e8971fb14a58add8b" + }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", "sourceCodeHash": "0xf22c94ed20c32a8ed2705a22d12c6969c3c3bad409c4efe2f95b0db74f210e10" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json new file mode 100644 index 00000000000..43e2fd35e17 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "proposals", + "offset": 0, + "slot": "0", + "type": "mapping(uint256 => struct Proposal)" + }, + { + "bytes": "32", + "label": "accountVotesSet", + "offset": 0, + "slot": "1", + "type": "mapping(uint256 => mapping(address => struct EnumerableSetUpgradeable.UintSet))" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index f7fdeefae8c..9ffdaf07452 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -1,51 +1,79 @@ [ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "1600", + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "uint256[50]" + }, { "bytes": "20", "label": "_owner", "offset": 0, - "slot": "0", + "slot": "51", "type": "address" }, + { + "bytes": "1568", + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "uint256[49]" + }, { "bytes": "32", "label": "minimumVotingPower", "offset": 0, - "slot": "1", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycleBlock", "offset": 0, - "slot": "2", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "3", + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "_proposalRequiredApprovals", "offset": 0, - "slot": "4", + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, { "bytes": "32", "label": "_proposalTypeData", "offset": 0, - "slot": "5", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "6", + "slot": "106", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 6661bc17030..714526bf480 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Contracts -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -11,11 +12,13 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +/// @custom:proxied true /// @title ProposalValidator /// @notice The ProposalValidator contract is responsible for validating proposals and moving /// them to the vote phase on the Optimism Governor. -contract ProposalValidator is Ownable { +contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -165,34 +168,49 @@ contract ProposalValidator is Ownable { /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; - /// @notice Initializes the ProposalValidator contract. - /// @param _owner The address that will own the contract. + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + function version() public pure virtual returns (string memory) { + return "1.0.0-beta.1"; + } + + /// @notice Constructs the ProposalValidator contract. + /// @param _attestationSchemaUid The schema UID for attestations in EAS. /// @param _governor The Optimism Governor contract address. /// @param _votingToken The token used to determine voting power. - /// @param _attestationSchemaUid The schema UID for attestations in EAS. + constructor( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _votingToken + ) + ReinitializableBase(1) + { + ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + GOVERNOR = _governor; + VOTING_TOKEN = _votingToken; + _disableInitializers(); + } + + /// @notice Initializes the ProposalValidator contract. + /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _votingCycleBlock The block number of the current voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. - constructor( + function initialize( address _owner, - IOptimismGovernor _governor, - IGovernanceToken _votingToken, - bytes32 _attestationSchemaUid, uint256 _minimumVotingPower, uint256 _votingCycleBlock, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals, ImmutableProposalTypeData[] memory _immutableProposalTypeDatas - ) { - transferOwnership(_owner); - GOVERNOR = _governor; - VOTING_TOKEN = _votingToken; - ATTESTATION_SCHEMA_UID = _attestationSchemaUid; - + ) + external + reinitializer(initVersion()) + { _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleBlock(_votingCycleBlock); _setDistributionThreshold(_distributionThreshold); @@ -201,6 +219,9 @@ contract ProposalValidator is Ownable { _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; } + + __Ownable_init(); + transferOwnership(_owner); } /// @notice Submit a proposal for delegate approval diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b58c5c7995b..fe6169bcc0f 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -6,9 +6,11 @@ import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; +import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -33,6 +35,7 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_D; ProposalValidator public validator; + ProposalValidator public impl; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; @@ -110,17 +113,25 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData ) = _getProposalTypesRequiredApprovalsAndImmutableData(); - validator = new ProposalValidator( - owner, - governor, - governanceToken, - ATTESTATION_SCHEMA_UID, - MINIMUM_VOTING_POWER, - VOTING_CYCLE_BLOCK, - DISTRIBUTION_THRESHOLD, - proposalTypes, - requiredApprovals, - immutableProposalTypeData + impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); + + validator = ProposalValidator(address(new Proxy(owner))); + + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + VOTING_CYCLE_BLOCK, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals, + immutableProposalTypeData + ) + ) ); topDelegate_A = _makeTopDelegate("topDelegate_A"); From c56734e59b103e629540c8896b32ba58a9621740 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 28 May 2025 01:56:04 -0300 Subject: [PATCH 38/73] fix: spec incosistencies (#398) * chore: remove votingCycleBlock variable * chore: remove ImmutableProposalTypeData --- .../governance/IProposalValidator.sol | 18 +---- .../snapshots/abi/ProposalValidator.json | 66 ------------------- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 20 +----- .../src/governance/ProposalValidator.sol | 41 +----------- .../test/governance/ProposalValidator.t.sol | 37 ++--------- 6 files changed, 14 insertions(+), 172 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 0034069fc25..da960c259b3 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -25,13 +25,7 @@ interface IProposalValidator is ISemver { mapping(address => bool) delegateApprovals; uint256 remainingApprovalsRequired; } - - struct ImmutableProposalTypeData { - address[] targets; - uint256[] values; - string[] signatures; - } - + enum ProposalType { ProtocolOrGovernorUpgrade, MaintenanceUpgrade, @@ -65,8 +59,6 @@ interface IProposalValidator is ISemver { event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - event VotingCycleBlockSet(uint256 newVotingCycleBlock); - event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); @@ -94,8 +86,6 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; - function setVotingCycleBlock(uint256 _votingCycleBlock) external; - function setDistributionThreshold(uint256 _distributionThreshold) external; function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; @@ -108,8 +98,6 @@ interface IProposalValidator is ISemver { function minimumVotingPower() external view returns (uint256); - function votingCycleBlock() external view returns (uint256); - function distributionThreshold() external view returns (uint256); function VOTING_TOKEN() external view returns (IGovernanceToken); @@ -125,11 +113,9 @@ interface IProposalValidator is ISemver { function initialize( address _owner, uint256 _minimumVotingPower, - uint256 _votingCycleBlock, uint256 _distributionThreshold, IProposalValidator.ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals, - IProposalValidator.ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + uint256[] memory _requiredApprovals ) external; function __constructor__( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 0430ce901f4..d22d4b2f105 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -129,11 +129,6 @@ "name": "_minimumVotingPower", "type": "uint256" }, - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - }, { "internalType": "uint256", "name": "_distributionThreshold", @@ -148,28 +143,6 @@ "internalType": "uint256[]", "name": "_requiredApprovals", "type": "uint256[]" - }, - { - "components": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "string[]", - "name": "signatures", - "type": "string[]" - } - ], - "internalType": "struct ProposalValidator.ImmutableProposalTypeData[]", - "name": "_immutableProposalTypeDatas", - "type": "tuple[]" } ], "name": "initialize", @@ -288,19 +261,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_votingCycleBlock", - "type": "uint256" - } - ], - "name": "setVotingCycleBlock", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -376,19 +336,6 @@ "stateMutability": "pure", "type": "function" }, - { - "inputs": [], - "name": "votingCycleBlock", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "anonymous": false, "inputs": [ @@ -559,19 +506,6 @@ "name": "ProposalSubmitted", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newVotingCycleBlock", - "type": "uint256" - } - ], - "name": "VotingCycleBlockSet", - "type": "event" - }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 5132c553bc3..d31b3c08357 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x79a40d1aa2eca36a8a8bcacfed58e42b6eca856e2e12898433a88d8aeaa6e74d", - "sourceCodeHash": "0x0049245cc58386fd48e72280d4d629d520c6c21ab061379e8971fb14a58add8b" + "initCodeHash": "0x295082116c7ed6b02211af266697b6e0839987eba410c4654051ad62b76e308e", + "sourceCodeHash": "0xd47d9c8bfbb130a8f1290dab1b25c4f92da9f7d0e9a5b7923dd646178713593f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 9ffdaf07452..b531f8f69b0 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -41,39 +41,25 @@ "slot": "101", "type": "uint256" }, - { - "bytes": "32", - "label": "votingCycleBlock", - "offset": 0, - "slot": "102", - "type": "uint256" - }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "103", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "_proposalRequiredApprovals", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, - { - "bytes": "32", - "label": "_proposalTypeData", - "offset": 0, - "slot": "105", - "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ImmutableProposalTypeData)" - }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "104", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 714526bf480..3512a160a4e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -61,16 +61,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 remainingApprovalsRequired; } - /// @notice Data structure for storing immutable proposal type data. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param signatures Function signatures for proposal calls. - struct ImmutableProposalTypeData { - address[] targets; - uint256[] values; - string[] signatures; - } - /*////////////////////////////////////////////////////////////// ENUMS //////////////////////////////////////////////////////////////*/ @@ -127,10 +117,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newMinimumVotingPower The new minimum voting power. event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - /// @notice Emitted when the voting cycle block is set. - /// @param newVotingCycleBlock The new voting cycle block. - event VotingCycleBlockSet(uint256 newVotingCycleBlock); - /// @notice Emitted when the distribution threshold is set. /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -153,18 +139,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The minimum voting power required for a delegate to approve proposals. uint256 public minimumVotingPower; - /// @notice The block number of the current voting cycle. - uint256 public votingCycleBlock; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; /// @notice The number of approvals required for each proposal type. mapping(ProposalType => uint256) private _proposalRequiredApprovals; - /// @notice The immutable data for each proposal type. - mapping(ProposalType => ImmutableProposalTypeData) private _proposalTypeData; - /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -194,30 +174,24 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. - /// @param _votingCycleBlock The block number of the current voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. - /// @param _immutableProposalTypeDatas Array of immutable proposal type data corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, - uint256 _votingCycleBlock, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals, - ImmutableProposalTypeData[] memory _immutableProposalTypeDatas + uint256[] memory _requiredApprovals ) external reinitializer(initVersion()) { _setMinimumVotingPower(_minimumVotingPower); - _setVotingCycleBlock(_votingCycleBlock); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); - _proposalTypeData[_proposalTypes[i]] = _immutableProposalTypeDatas[i]; } __Ownable_init(); @@ -342,12 +316,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setMinimumVotingPower(_minimumVotingPower); } - /// @notice Sets the block number of the current voting cycle. - /// @param _votingCycleBlock The new voting cycle block number. - function setVotingCycleBlock(uint256 _votingCycleBlock) external onlyOwner { - _setVotingCycleBlock(_votingCycleBlock); - } - /// @notice Sets the max amount of tokens that can be distributed in a proposal. /// @param _distributionThreshold The new distribution threshold. function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { @@ -434,13 +402,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit MinimumVotingPowerSet(_minimumVotingPower); } - /// @notice Private function to set the voting cycle block and emit event. - /// @param _votingCycleBlock The new voting cycle block number. - function _setVotingCycleBlock(uint256 _votingCycleBlock) private { - votingCycleBlock = _votingCycleBlock; - emit VotingCycleBlockSet(_votingCycleBlock); - } - /// @notice Private function to set the distribution threshold and emit event. /// @param _distributionThreshold The new distribution threshold. function _setDistributionThreshold(uint256 _distributionThreshold) private { diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fe6169bcc0f..52815bce0bb 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -22,7 +22,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP - uint256 public constant VOTING_CYCLE_BLOCK = 100; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; @@ -61,14 +60,10 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } - function _getProposalTypesRequiredApprovalsAndImmutableData() + function _getProposalTypesRequiredApprovals() internal pure - returns ( - ProposalValidator.ProposalType[] memory, - uint256[] memory, - ProposalValidator.ImmutableProposalTypeData[] memory - ) + returns (ProposalValidator.ProposalType[] memory, uint256[] memory) { ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -84,15 +79,7 @@ contract ProposalValidator_Init is CommonTest { requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; - ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData = - new ProposalValidator.ImmutableProposalTypeData[](5); - immutableProposalTypeData[0] = ProposalValidator.ImmutableProposalTypeData({ - targets: new address[](1), - values: new uint256[](1), - signatures: new string[](1) - }); - - return (proposalTypes, requiredApprovals, immutableProposalTypeData); + return (proposalTypes, requiredApprovals); } /// @dev Sets up the test suite. @@ -107,11 +94,8 @@ contract ProposalValidator_Init is CommonTest { "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false ); - ( - ProposalValidator.ProposalType[] memory proposalTypes, - uint256[] memory requiredApprovals, - ProposalValidator.ImmutableProposalTypeData[] memory immutableProposalTypeData - ) = _getProposalTypesRequiredApprovalsAndImmutableData(); + (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = + _getProposalTypesRequiredApprovals(); impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); @@ -121,16 +105,7 @@ contract ProposalValidator_Init is CommonTest { IProxy(payable(address(validator))).upgradeToAndCall( address(impl), abi.encodeCall( - impl.initialize, - ( - owner, - MINIMUM_VOTING_POWER, - VOTING_CYCLE_BLOCK, - DISTRIBUTION_THRESHOLD, - proposalTypes, - requiredApprovals, - immutableProposalTypeData - ) + impl.initialize, (owner, MINIMUM_VOTING_POWER, DISTRIBUTION_THRESHOLD, proposalTypes, requiredApprovals) ) ); From 1506286f14e4259525d146f53c9466c3f025f4bb Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 28 May 2025 11:57:38 -0300 Subject: [PATCH 39/73] feat: admin functions (#393) * feat: add upgradeability to ProposalValidator contract * chore: fix styling * feat: add admin functions and tests * chore: more descriptive variables naming * test: expect event emissions * test: use fuzzing for setter functions * chore: run pre-pr * docs: use correct natspec in ProosalValidator contract * feat: add semver * feat: add reinitializable base * chore: run pre-pr * chore: run pre-pr --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 51 +++- .../snapshots/abi/ProposalValidator.json | 172 +++++++++++-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 11 +- .../src/governance/ProposalValidator.sol | 98 +++++++- .../test/governance/ProposalValidator.t.sol | 231 +++++++++++++++++- 6 files changed, 515 insertions(+), 52 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index da960c259b3..244044dfb6c 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -15,6 +15,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); + error ProposalValidator_VotingCycleAlreadySet(); error ReinitializableBase_ZeroInitVersion(); struct ProposalData { @@ -61,8 +62,15 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); - + event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + + event VotingCycleDataSet( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); + event Initialized(uint8 version); function submitProposal( @@ -87,8 +95,27 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; - - function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) external; + + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) external; + + function initialize( + address _owner, + uint256 _minimumVotingPower, + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit, + uint256 _distributionThreshold, + ProposalType[] memory _proposalTypes, + uint256[] memory _requiredApprovals + ) external; function renounceOwnership() external; @@ -109,14 +136,14 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - - function initialize( - address _owner, - uint256 _minimumVotingPower, - uint256 _distributionThreshold, - IProposalValidator.ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals - ) external; + + function proposalRequiredApprovals(ProposalType) external view returns (uint256); + + function votingCycles(uint256) external view returns ( + uint256 startingBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); function __constructor__( bytes32 _attestationSchemaUid, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index d22d4b2f105..efe0ba90eef 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -129,6 +129,26 @@ "name": "_minimumVotingPower", "type": "uint256" }, + { + "internalType": "uint256", + "name": "_cycleNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleDistributionLimit", + "type": "uint256" + }, { "internalType": "uint256", "name": "_distributionThreshold", @@ -210,6 +230,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "", + "type": "uint8" + } + ], + "name": "proposalRequiredApprovals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "renounceOwnership", @@ -256,7 +295,35 @@ "type": "uint256" } ], - "name": "setProposalRequiredApprovals", + "name": "setProposalTypeApprovalThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_cycleNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_votingCycleDistributionLimit", + "type": "uint256" + } + ], + "name": "setVotingCycleData", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -336,6 +403,35 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "votingCycles", + "outputs": [ + { + "internalType": "uint256", + "name": "startingBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "votingCycleDistributionLimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "anonymous": false, "inputs": [ @@ -394,25 +490,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ProposalValidator.ProposalType", - "name": "proposalType", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "newApprovalThreshold", - "type": "uint256" - } - ], - "name": "ProposalApprovalThresholdSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -506,6 +583,56 @@ "name": "ProposalSubmitted", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newApprovalThreshold", + "type": "uint256" + } + ], + "name": "ProposalTypeApprovalThresholdSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cycleNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "votingCycleDistributionLimit", + "type": "uint256" + } + ], + "name": "VotingCycleDataSet", + "type": "event" + }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", @@ -536,6 +663,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_VotingCycleAlreadySet", + "type": "error" + }, { "inputs": [], "name": "ReinitializableBase_ZeroInitVersion", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index d31b3c08357..a371f774ef0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x295082116c7ed6b02211af266697b6e0839987eba410c4654051ad62b76e308e", - "sourceCodeHash": "0xd47d9c8bfbb130a8f1290dab1b25c4f92da9f7d0e9a5b7923dd646178713593f" + "initCodeHash": "0x218b5001f56c04cefe63991cdfa08d79595e94ba203dd615818f3e5c570d9f26", + "sourceCodeHash": "0xd9cbe54c1f1c0152ee3da444f39f4cf6b0f6087d6b8c3da18ed8beb830835029" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index b531f8f69b0..cf455740bef 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -50,16 +50,23 @@ }, { "bytes": "32", - "label": "_proposalRequiredApprovals", + "label": "votingCycles", "offset": 0, "slot": "103", + "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" + }, + { + "bytes": "32", + "label": "proposalRequiredApprovals", + "offset": 0, + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => uint256)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "104", + "slot": "105", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3512a160a4e..26d1b5e8add 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -38,6 +38,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); + /// @notice Thrown when a voting cycle is already set. + error ProposalValidator_VotingCycleAlreadySet(); + /// @notice Thrown when a proposal does not exist. error ProposalValidator_ProposalDoesNotExist(); @@ -61,6 +64,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 remainingApprovalsRequired; } + /// @notice Data structure for storing voting cycle data. + /// @param startingBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. + struct VotingCycleData { + uint256 startingBlock; + uint256 duration; + uint256 votingCycleDistributionLimit; + } + /*////////////////////////////////////////////////////////////// ENUMS //////////////////////////////////////////////////////////////*/ @@ -117,6 +130,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newMinimumVotingPower The new minimum voting power. event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + /// @notice Emitted when the voting cycle data is set. + /// @param cycleNumber The number of the voting cycle. + /// @param startBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + /// @notice Emitted when the distribution threshold is set. /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -124,7 +146,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the number of approvals required for a proposal type is set. /// @param proposalType The type of proposal. /// @param newApprovalThreshold The new approval threshold. - event ProposalApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -142,8 +164,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; + /// @notice Mapping of voting cycle numbers to their corresponding data. + mapping(uint256 => VotingCycleData) public votingCycles; + /// @notice The number of approvals required for each proposal type. - mapping(ProposalType => uint256) private _proposalRequiredApprovals; + mapping(ProposalType => uint256) public proposalRequiredApprovals; /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -174,12 +199,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. + /// @param _cycleNumber The number of the current voting cycle. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set approval thresholds for. /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, uint256[] memory _requiredApprovals @@ -188,10 +221,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { reinitializer(initVersion()) { _setMinimumVotingPower(_minimumVotingPower); + _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { - _setProposalRequiredApprovals(_proposalTypes[i], _requiredApprovals[i]); + _setProposalTypeApprovalThreshold(_proposalTypes[i], _requiredApprovals[i]); } __Ownable_init(); @@ -316,6 +350,23 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setMinimumVotingPower(_minimumVotingPower); } + /// @notice Sets the data of a voting cycle. + /// @param _cycleNumber The number of the voting cycle to set. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) + external + onlyOwner + { + _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + } + /// @notice Sets the max amount of tokens that can be distributed in a proposal. /// @param _distributionThreshold The new distribution threshold. function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { @@ -325,8 +376,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Sets the number of approvals required for each proposal type. /// @param _proposalType The type of proposal to set the required approvals for. /// @param _requiredApprovals The new required approvals. - function setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) external onlyOwner { - _setProposalRequiredApprovals(_proposalType, _requiredApprovals); + function setProposalTypeApprovalThreshold( + ProposalType _proposalType, + uint256 _requiredApprovals + ) + external + onlyOwner + { + _setProposalTypeApprovalThreshold(_proposalType, _requiredApprovals); } /// @notice Validates a proposal before submission. @@ -402,6 +459,31 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit MinimumVotingPowerSet(_minimumVotingPower); } + /// @notice Private function to set the voting cycle data and emit event. + /// @param _cycleNumber The number of the voting cycle to set. + /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _duration The duration of the voting cycle. + /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + function _setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) + private + { + if (votingCycles[_cycleNumber].startingBlock != 0) { + revert ProposalValidator_VotingCycleAlreadySet(); + } + + votingCycles[_cycleNumber] = VotingCycleData({ + startingBlock: _startBlock, + duration: _duration, + votingCycleDistributionLimit: _votingCycleDistributionLimit + }); + emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + } + /// @notice Private function to set the distribution threshold and emit event. /// @param _distributionThreshold The new distribution threshold. function _setDistributionThreshold(uint256 _distributionThreshold) private { @@ -412,8 +494,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Private function to set a proposal's type required approvals and emit event. /// @param _proposalType The type of proposal to set the required approvals for. /// @param _requiredApprovals The new required approvals. - function _setProposalRequiredApprovals(ProposalType _proposalType, uint256 _requiredApprovals) private { - _proposalRequiredApprovals[_proposalType] = _requiredApprovals; - emit ProposalApprovalThresholdSet(_proposalType, _requiredApprovals); + function _setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) private { + proposalRequiredApprovals[_proposalType] = _requiredApprovals; + emit ProposalTypeApprovalThresholdSet(_proposalType, _requiredApprovals); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 52815bce0bb..295781bbdd5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Interfaces import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; @@ -18,10 +19,39 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; +/// @title ProposalValidatorForTest +/// @notice A test contract that exposes the private _hashProposal function +contract ProposalValidatorForTest is ProposalValidator { + constructor( + bytes32 _attestationSchemaUid, + IOptimismGovernor _governor, + IGovernanceToken _governanceToken + ) + ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) + { } + + function hashProposal( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) + public + pure + returns (bytes32) + { + return _hashProposal(_targets, _values, _calldatas, _description); + } +} + /// @title ProposalValidator_Init /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP + uint256 public constant CYCLE_NUMBER = 1; + uint256 public constant START_BLOCK = 1000000; + uint256 public constant DURATION = 100; + uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; @@ -33,12 +63,31 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_C; address topDelegate_D; - ProposalValidator public validator; - ProposalValidator public impl; + ProposalValidatorForTest public validator; + ProposalValidatorForTest public impl; IOptimismGovernor public governor; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + address[] targets, + uint256[] values, + bytes[] calldatas, + string description, + ProposalValidator.ProposalType proposalType, + uint8 proposalTypeConfigurator + ); + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalTypeApprovalThresholdSet(ProposalValidator.ProposalType proposalType, uint256 newApprovalThreshold); + /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { vm.mockCall(_receiver, _calldata, _returned); @@ -97,15 +146,26 @@ contract ProposalValidator_Init is CommonTest { (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = _getProposalTypesRequiredApprovals(); - impl = new ProposalValidator(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); - validator = ProposalValidator(address(new Proxy(owner))); + validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), abi.encodeCall( - impl.initialize, (owner, MINIMUM_VOTING_POWER, DISTRIBUTION_THRESHOLD, proposalTypes, requiredApprovals) + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + requiredApprovals + ) ) ); @@ -170,6 +230,20 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); + + // Expect event to be emitted + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedProposalHash, + topDelegate_A, + _targets, + _values, + _calldatas, + _description, + proposalType, + proposalTypeConfigurator + ); // Submit the proposal vm.prank(topDelegate_A); @@ -177,7 +251,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid ); - assertEq(proposalHash, keccak256(abi.encode(_targets, _values, _calldatas, _description))); + assertEq(proposalHash, expectedProposalHash); } } @@ -237,9 +311,24 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { } function test_approveProposal_succeeds() public { + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_A); _approveProposal(topDelegate_A, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_B); _approveProposal(topDelegate_B, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_C); _approveProposal(topDelegate_C, proposalHash); + + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_D); _approveProposal(topDelegate_D, proposalHash); } } @@ -315,6 +404,10 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { abi.encode(1) ); + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(proposalHash, owner); + vm.prank(owner); uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); @@ -397,7 +490,100 @@ contract ProposalValidator_Getters_Test is ProposalValidator_Init { /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { -// TODO: Implement tests for setters + function testFuzz_setMinimumVotingPower_succeeds(uint256 newMinimumVotingPower) public { + // Expect the MinimumVotingPowerSet event to be emitted + vm.expectEmit(address(validator)); + emit MinimumVotingPowerSet(newMinimumVotingPower); + + vm.prank(owner); + validator.setMinimumVotingPower(newMinimumVotingPower); + + assertEq(validator.minimumVotingPower(), newMinimumVotingPower); + } + + function test_setMinimumVotingPower_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setMinimumVotingPower(10000 ether); + } + + function testFuzz_setVotingCycleData_succeeds( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle + + // Expect the VotingCycleDataSet event to be emitted + vm.expectEmit(address(validator)); + emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); + + vm.prank(owner); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + + (uint256 actualStartBlock, uint256 actualDuration, uint256 actualDistributionLimit) = + validator.votingCycles(cycleNumber); + + assertEq(actualStartBlock, startBlock); + assertEq(actualDuration, duration); + assertEq(actualDistributionLimit, distributionLimit); + } + + function test_setVotingCycleData_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + } + + function test_setVotingCycleData_votingCycleAlreadySet_reverts() public { + vm.prank(owner); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + + vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); + vm.prank(owner); + validator.setVotingCycleData(2, block.number, 100, 10000 ether); + } + + function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { + // Expect the DistributionThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit DistributionThresholdSet(newDistributionThreshold); + + vm.prank(owner); + validator.setDistributionThreshold(newDistributionThreshold); + + assertEq(validator.distributionThreshold(), newDistributionThreshold); + } + + function test_setDistributionThreshold_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setDistributionThreshold(10000 ether); + } + + function testFuzz_setProposalTypeApprovalThreshold_succeeds(uint8 proposalTypeValue, uint256 newThreshold) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Expect the ProposalTypeApprovalThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit ProposalTypeApprovalThresholdSet(proposalType, newThreshold); + + vm.prank(owner); + validator.setProposalTypeApprovalThreshold(proposalType, newThreshold); + + assertEq(validator.proposalRequiredApprovals(proposalType), newThreshold); + } + + function test_setProposalTypeApprovalThreshold_notOwner_reverts() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setProposalTypeApprovalThreshold(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, 4); + } } /// @title ProposalValidator_Integration_Test @@ -412,15 +598,40 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { uint8 proposalTypeConfigurator = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Expect ProposalSubmitted event + bytes32 expectedProposalHash = keccak256(abi.encode(targets, values, calldatas, description)); + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedProposalHash, + topDelegate_A, + targets, + values, + calldatas, + description, + proposalType, + proposalTypeConfigurator + ); + vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitProposal( targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid ); - // Collect all required approvals + // Expect ProposalApproved events for each approval + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_A); _approveProposal(topDelegate_A, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_B); _approveProposal(topDelegate_B, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_C); _approveProposal(topDelegate_C, proposalHash); + + vm.expectEmit(address(validator)); + emit ProposalApproved(proposalHash, topDelegate_D); _approveProposal(topDelegate_D, proposalHash); // Mock the governor call @@ -432,6 +643,10 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { abi.encode(1) ); + // Expect ProposalMovedToVote event + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(proposalHash, owner); + // Move to vote phase vm.prank(owner); uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); From d65062a1972c3770648805c77595c89d522e9a52 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:03:12 -0300 Subject: [PATCH 40/73] refactor: proposal type data struct (#401) * refactor: use proposalTypesData mapping * docs: improve natspec * feat: add mismatched lenghts check in contract initializer * fix: emit ProposalTypeDataSet with missing arg * refactor: correct naming in test helper functions * refactor: correct variable naming in fuzz tests * perf: remove redundant fields from ProposalData struct * chore: run pre-pr * chore: improve code formatting --- .../governance/IProposalValidator.sol | 21 +- .../snapshots/abi/ProposalValidator.json | 71 ++++-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 4 +- .../src/governance/ProposalValidator.sol | 84 ++++--- .../test/governance/ProposalValidator.t.sol | 237 +++++++++++++----- 6 files changed, 299 insertions(+), 122 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 244044dfb6c..00481da61ec 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -16,15 +16,20 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidAttestation(); error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_VotingCycleAlreadySet(); + error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); struct ProposalData { address proposer; ProposalType proposalType; - uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; - uint256 remainingApprovalsRequired; + uint256 approvalCount; + } + + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalTypeConfigurator; } enum ProposalType { @@ -62,7 +67,7 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); event VotingCycleDataSet( uint256 cycleNumber, @@ -79,7 +84,6 @@ interface IProposalValidator is ISemver { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, - uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external returns (bytes32 proposalHash_); @@ -96,7 +100,10 @@ interface IProposalValidator is ISemver { function setDistributionThreshold(uint256 _distributionThreshold) external; - function setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) external; + function setProposalTypeData( + ProposalType _proposalType, + ProposalTypeData memory _proposalTypeData + ) external; function setVotingCycleData( uint256 _cycleNumber, @@ -114,7 +121,7 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals + ProposalTypeData[] memory _proposalTypesData ) external; function renounceOwnership() external; @@ -137,7 +144,7 @@ interface IProposalValidator is ISemver { function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function proposalRequiredApprovals(ProposalType) external view returns (uint256); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalTypeConfigurator); function votingCycles(uint256) external view returns ( uint256 startingBlock, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index efe0ba90eef..edc205a5ad5 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -160,9 +160,21 @@ "type": "uint8[]" }, { - "internalType": "uint256[]", - "name": "_requiredApprovals", - "type": "uint256[]" + "components": [ + { + "internalType": "uint256", + "name": "requiredApprovals", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" + } + ], + "internalType": "struct ProposalValidator.ProposalTypeData[]", + "name": "_proposalTypesData", + "type": "tuple[]" } ], "name": "initialize", @@ -238,12 +250,17 @@ "type": "uint8" } ], - "name": "proposalRequiredApprovals", + "name": "proposalTypesData", "outputs": [ { "internalType": "uint256", - "name": "", + "name": "requiredApprovals", "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], "stateMutability": "view", @@ -290,12 +307,24 @@ "type": "uint8" }, { - "internalType": "uint256", - "name": "_requiredApprovals", - "type": "uint256" - } - ], - "name": "setProposalTypeApprovalThreshold", + "components": [ + { + "internalType": "uint256", + "name": "requiredApprovals", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" + } + ], + "internalType": "struct ProposalValidator.ProposalTypeData", + "name": "_proposalTypeData", + "type": "tuple" + } + ], + "name": "setProposalTypeData", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -355,11 +384,6 @@ "name": "_proposalType", "type": "uint8" }, - { - "internalType": "uint8", - "name": "_proposalTypeConfigurator", - "type": "uint8" - }, { "internalType": "bytes32", "name": "_attestationUid", @@ -595,11 +619,17 @@ { "indexed": false, "internalType": "uint256", - "name": "newApprovalThreshold", + "name": "requiredApprovals", "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "proposalTypeConfigurator", + "type": "uint8" } ], - "name": "ProposalTypeApprovalThresholdSet", + "name": "ProposalTypeDataSet", "type": "event" }, { @@ -663,6 +693,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalTypesDataLengthMismatch", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_VotingCycleAlreadySet", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index a371f774ef0..ac9f644132a 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x218b5001f56c04cefe63991cdfa08d79595e94ba203dd615818f3e5c570d9f26", - "sourceCodeHash": "0xd9cbe54c1f1c0152ee3da444f39f4cf6b0f6087d6b8c3da18ed8beb830835029" + "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", + "sourceCodeHash": "0xd7d94a765bec0d80cac4bce4e62270a27a830b103dd554d7a4be540a49f8f4d5" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index cf455740bef..8b940980418 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -57,10 +57,10 @@ }, { "bytes": "32", - "label": "proposalRequiredApprovals", + "label": "proposalTypesData", "offset": 0, "slot": "104", - "type": "mapping(enum ProposalValidator.ProposalType => uint256)" + "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 26d1b5e8add..c2b4ab06328 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -44,27 +44,36 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when a proposal does not exist. error ProposalValidator_ProposalDoesNotExist(); + /// @notice Thrown when the length of the proposal types and proposal types data arrays do not match. + error ProposalValidator_ProposalTypesDataLengthMismatch(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ - /// @notice Data structure for storing proposal information. + /// @notice Struct for storing proposal information. /// @param proposer The address that submitted the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. - /// @param proposalTypeConfigurator Configuration value specific to the proposal type. /// @param inVoting Whether the proposal has been moved to the voting phase. /// @param delegateApprovals Mapping of delegate addresses to their approval status. - /// @param remainingApprovalsRequired Number of approvals still needed before voting. + /// @param approvalCount Number of approvals received so far. struct ProposalData { address proposer; ProposalType proposalType; - uint8 proposalTypeConfigurator; bool inVoting; mapping(address => bool) delegateApprovals; - uint256 remainingApprovalsRequired; + uint256 approvalCount; + } + + /// @notice Struct for storing explicit data for each proposal type. + /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for voting. + /// @param proposalTypeConfigurator The voting module each proposal type must use. + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalTypeConfigurator; } - /// @notice Data structure for storing voting cycle data. + /// @notice Struct for storing voting cycle data. /// @param startingBlock The block number of the starting block of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. @@ -143,10 +152,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param newDistributionThreshold The new distribution threshold. event DistributionThresholdSet(uint256 newDistributionThreshold); - /// @notice Emitted when the number of approvals required for a proposal type is set. + /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. - /// @param newApprovalThreshold The new approval threshold. - event ProposalTypeApprovalThresholdSet(ProposalType proposalType, uint256 newApprovalThreshold); + /// @param requiredApprovals The required number of approvals. + /// @param proposalTypeConfigurator The proposal type configurator. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -167,8 +177,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of voting cycle numbers to their corresponding data. mapping(uint256 => VotingCycleData) public votingCycles; - /// @notice The number of approvals required for each proposal type. - mapping(ProposalType => uint256) public proposalRequiredApprovals; + /// @notice Mapping of proposal types to their corresponding data. + mapping(ProposalType => ProposalTypeData) public proposalTypesData; /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) private _proposals; @@ -204,8 +214,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. - /// @param _proposalTypes Array of proposal types to set approval thresholds for. - /// @param _requiredApprovals Array of approval thresholds corresponding to the proposal types. + /// @param _proposalTypes Array of proposal types to set data for. + /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, uint256 _minimumVotingPower, @@ -215,17 +225,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, ProposalType[] memory _proposalTypes, - uint256[] memory _requiredApprovals + ProposalTypeData[] memory _proposalTypesData ) external reinitializer(initVersion()) { + if (_proposalTypes.length != _proposalTypesData.length) { + revert ProposalValidator_ProposalTypesDataLengthMismatch(); + } + _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { - _setProposalTypeApprovalThreshold(_proposalTypes[i], _requiredApprovals[i]); + _setProposalTypeData(_proposalTypes[i], _proposalTypesData[i]); } __Ownable_init(); @@ -245,7 +259,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes[] memory _calldatas, string memory _description, ProposalType _proposalType, - uint8 _proposalTypeConfigurator, bytes32 _attestationUid ) external @@ -260,11 +273,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadySubmitted(); } + ProposalTypeData memory proposalTypeData = proposalTypesData[_proposalType]; + proposal.proposer = msg.sender; proposal.proposalType = _proposalType; - proposal.proposalTypeConfigurator = _proposalTypeConfigurator; proposal.inVoting = false; - proposal.remainingApprovalsRequired = 4; // Hardcoded for now, will change with proposalTypes emit ProposalSubmitted( proposalHash_, @@ -274,7 +287,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _calldatas, _description, _proposalType, - _proposalTypeConfigurator + proposalTypeData.proposalTypeConfigurator ); } @@ -292,7 +305,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposal.delegateApprovals[msg.sender] = true; - proposal.remainingApprovalsRequired--; // Expected overflow when all approvals are granted + proposal.approvalCount++; emit ProposalApproved(_proposalHash, msg.sender); } @@ -321,7 +334,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalDoesNotExist(); } - if (proposal.remainingApprovalsRequired > 0) { + ProposalTypeData memory proposalTypeData = proposalTypesData[proposal.proposalType]; + if (proposal.approvalCount < proposalTypeData.requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } @@ -332,7 +346,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposal.proposalTypeConfigurator); + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalTypeConfigurator); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -373,17 +387,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setDistributionThreshold(_distributionThreshold); } - /// @notice Sets the number of approvals required for each proposal type. - /// @param _proposalType The type of proposal to set the required approvals for. - /// @param _requiredApprovals The new required approvals. - function setProposalTypeApprovalThreshold( + /// @notice Sets the data for a proposal type. + /// @param _proposalType The type of proposal to set the data for. + /// @param _proposalTypeData The data for the proposal type. + function setProposalTypeData( ProposalType _proposalType, - uint256 _requiredApprovals + ProposalTypeData memory _proposalTypeData ) external onlyOwner { - _setProposalTypeApprovalThreshold(_proposalType, _requiredApprovals); + _setProposalTypeData(_proposalType, _proposalTypeData); } /// @notice Validates a proposal before submission. @@ -491,11 +505,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit DistributionThresholdSet(_distributionThreshold); } - /// @notice Private function to set a proposal's type required approvals and emit event. - /// @param _proposalType The type of proposal to set the required approvals for. - /// @param _requiredApprovals The new required approvals. - function _setProposalTypeApprovalThreshold(ProposalType _proposalType, uint256 _requiredApprovals) private { - proposalRequiredApprovals[_proposalType] = _requiredApprovals; - emit ProposalTypeApprovalThresholdSet(_proposalType, _requiredApprovals); + /// @notice Private function to set a proposal's type data. + /// @param _proposalType The type of proposal to set the data for. + /// @param _proposalTypeData The data for the proposal type. + function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { + proposalTypesData[_proposalType] = _proposalTypeData; + emit ProposalTypeDataSet( + _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalTypeConfigurator + ); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 295781bbdd5..b0c16baf572 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -86,7 +86,9 @@ contract ProposalValidator_Init is CommonTest { uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit ); event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeApprovalThresholdSet(ProposalValidator.ProposalType proposalType, uint256 newApprovalThreshold); + event ProposalTypeDataSet( + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator + ); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -109,10 +111,10 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } - function _getProposalTypesRequiredApprovals() + function _getProposalTypesAndData() internal pure - returns (ProposalValidator.ProposalType[] memory, uint256[] memory) + returns (ProposalValidator.ProposalType[] memory, ProposalValidator.ProposalTypeData[] memory) { ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](5); proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -121,30 +123,37 @@ contract ProposalValidator_Init is CommonTest { proposalTypes[3] = ProposalValidator.ProposalType.GovernanceFund; proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; - uint256[] memory requiredApprovals = new uint256[](5); - requiredApprovals[0] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[1] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[2] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[3] = PROPOSAL_REQUIRED_APPROVALS; - requiredApprovals[4] = PROPOSAL_REQUIRED_APPROVALS; - - return (proposalTypes, requiredApprovals); - } - - /// @dev Sets up the test suite. - function setUp() public virtual override { - super.setUp(); - owner = governanceToken.owner(); - rando = makeAddr("rando"); - governor = IOptimismGovernor(makeAddr("governor")); - - vm.prank(owner); - ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false - ); - - (ProposalValidator.ProposalType[] memory proposalTypes, uint256[] memory requiredApprovals) = - _getProposalTypesRequiredApprovals(); + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[2] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[3] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[4] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + + return (proposalTypes, proposalTypesData); + } + + /// @notice Initializes the validator + function _initializeValidator() internal virtual { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); @@ -164,10 +173,25 @@ contract ProposalValidator_Init is CommonTest { DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, proposalTypes, - requiredApprovals + proposalTypesData ) ) ); + } + + /// @dev Sets up the test suite. + function setUp() public virtual override { + super.setUp(); + owner = governanceToken.owner(); + rando = makeAddr("rando"); + governor = IOptimismGovernor(makeAddr("governor")); + + vm.prank(owner); + ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + ); + + _initializeValidator(); topDelegate_A = _makeTopDelegate("topDelegate_A"); topDelegate_B = _makeTopDelegate("topDelegate_B"); @@ -247,9 +271,8 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { // Submit the proposal vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitProposal( - _targets, _values, _calldatas, _description, proposalType, proposalTypeConfigurator, attestationUid - ); + bytes32 proposalHash = + validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); assertEq(proposalHash, expectedProposalHash); } @@ -268,9 +291,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, invalidAttestationUid - ); + validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); } function test_submitProposal_wrongAttester_reverts() public { @@ -285,9 +306,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } } @@ -305,9 +324,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_approveProposal_succeeds() public { @@ -347,9 +364,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_approveProposal_insufficientVotingPower_reverts() public { @@ -385,9 +400,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); _approveProposal(topDelegate_A, proposalHash); _approveProposal(topDelegate_B, proposalHash); @@ -435,9 +448,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); - proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); } function test_moveToVote_insufficientApprovals_reverts() public { @@ -564,25 +575,41 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { validator.setDistributionThreshold(10000 ether); } - function testFuzz_setProposalTypeApprovalThreshold_succeeds(uint8 proposalTypeValue, uint256 newThreshold) public { + function testFuzz_setProposalTypeData_succeeds( + uint8 proposalTypeValue, + uint256 newRequiredApprovals, + uint8 newConfigurator + ) + public + { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Expect the ProposalTypeApprovalThresholdSet event to be emitted + ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ + requiredApprovals: newRequiredApprovals, + proposalTypeConfigurator: newConfigurator + }); + + // Expect the ProposalTypeDataSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalTypeApprovalThresholdSet(proposalType, newThreshold); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newConfigurator); vm.prank(owner); - validator.setProposalTypeApprovalThreshold(proposalType, newThreshold); + validator.setProposalTypeData(proposalType, newData); - assertEq(validator.proposalRequiredApprovals(proposalType), newThreshold); + (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalType); + assertEq(requiredApprovals, newRequiredApprovals); + assertEq(proposalTypeConfigurator, newConfigurator); } - function test_setProposalTypeApprovalThreshold_notOwner_reverts() public { + function test_setProposalTypeData_notOwner_reverts() public { + ProposalValidator.ProposalTypeData memory newData = + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalTypeConfigurator: 0 }); + vm.prank(rando); vm.expectRevert("Ownable: caller is not the owner"); - validator.setProposalTypeApprovalThreshold(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, 4); + validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } } @@ -613,9 +640,8 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { ); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitProposal( - targets, values, calldatas, description, proposalType, proposalTypeConfigurator, attestationUid - ); + bytes32 proposalHash = + validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); // Expect ProposalApproved events for each approval vm.expectEmit(address(validator)); @@ -657,3 +683,96 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { validator.moveToVote(targets, values, calldatas, description); } } + +/// @title ProposalValidator_Initialize_Test +/// @notice Tests for the initialize function +contract ProposalValidator_Initialize_Test is ProposalValidator_Init { + /// @dev Override to create validator proxy without initialization for testing + function _initializeValidator() internal override { + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + validator = ProposalValidatorForTest(address(new Proxy(owner))); + // Initialize will be tested manually + } + + function test_initialize_succeeds() public { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); + + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); + + // Verify initialization was successful + assertEq(validator.minimumVotingPower(), MINIMUM_VOTING_POWER); + assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.owner(), owner); + + // Verify voting cycle data + (uint256 startBlock, uint256 duration, uint256 distributionLimit) = validator.votingCycles(CYCLE_NUMBER); + assertEq(startBlock, START_BLOCK); + assertEq(duration, DURATION); + assertEq(distributionLimit, DISTRIBUTION_LIMIT); + + // Verify proposal type data + for (uint256 i = 0; i < proposalTypes.length; i++) { + (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalTypes[i]); + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + assertEq(proposalTypeConfigurator, 0); + } + } + + function test_initialize_mismatchedArrayLengths_reverts() public { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + + // Create mismatched array with different length + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalTypeConfigurator: 0 + }); + + vm.prank(owner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + MINIMUM_VOTING_POWER, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); + } +} From 928d95e3343f46c65c01339d5ac4d094ba2ff38b Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:37:55 -0300 Subject: [PATCH 41/73] refactor: voting module naming (#404) * refactor: rename proposalTypeConfigurator to proposalVotingModule * chore: run pre-pr * fix: missing rename in comment * chore: run pre-pr * docs: fix natspec description --- .../governance/IProposalValidator.sol | 8 +-- .../snapshots/abi/ProposalValidator.json | 10 +-- .../snapshots/semver-lock.json | 2 +- .../src/governance/ProposalValidator.sol | 21 +++--- .../test/governance/ProposalValidator.t.sol | 66 +++++++++---------- 5 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 00481da61ec..7ab9b4194a5 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -29,7 +29,7 @@ interface IProposalValidator is ISemver { struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; } enum ProposalType { @@ -48,7 +48,7 @@ interface IProposalValidator is ISemver { bytes[] calldatas, string description, ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); event ProposalApproved( @@ -67,7 +67,7 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); event VotingCycleDataSet( uint256 cycleNumber, @@ -144,7 +144,7 @@ interface IProposalValidator is ISemver { function ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalTypeConfigurator); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( uint256 startingBlock, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index edc205a5ad5..acb1665cd84 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -168,7 +168,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -259,7 +259,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -315,7 +315,7 @@ }, { "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -600,7 +600,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], @@ -625,7 +625,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalTypeConfigurator", + "name": "proposalVotingModule", "type": "uint8" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index ac9f644132a..2e38b0ebd96 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -173,7 +173,7 @@ }, "src/governance/ProposalValidator.sol:ProposalValidator": { "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", - "sourceCodeHash": "0xd7d94a765bec0d80cac4bce4e62270a27a830b103dd554d7a4be540a49f8f4d5" + "sourceCodeHash": "0xddf3e6506f0155d0120e467cb1437c49b1eff28d362b8e7de55101cd91e43427" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c2b4ab06328..dcb729480be 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -66,11 +66,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Struct for storing explicit data for each proposal type. - /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for voting. - /// @param proposalTypeConfigurator The voting module each proposal type must use. + /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for + /// voting. + /// @param proposalVotingModule The voting module each proposal type must use. struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; } /// @notice Struct for storing voting cycle data. @@ -113,7 +114,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param calldatas Function data for proposal calls. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. - /// @param proposalTypeConfigurator Configuration value specific to the proposal type. + /// @param proposalVotingModule Voting module specific to the proposal type. event ProposalSubmitted( bytes32 indexed proposalHash, address indexed proposer, @@ -122,7 +123,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes[] calldatas, string description, ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); /// @notice Emitted when a delegate approves a proposal. @@ -155,8 +156,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalTypeConfigurator The proposal type configurator. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator); + /// @param proposalVotingModule The proposal voting module. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -287,7 +288,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _calldatas, _description, _proposalType, - proposalTypeData.proposalTypeConfigurator + proposalTypeData.proposalVotingModule ); } @@ -346,7 +347,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalTypeConfigurator); + GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalVotingModule); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -511,7 +512,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { proposalTypesData[_proposalType] = _proposalTypeData; emit ProposalTypeDataSet( - _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalTypeConfigurator + _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalVotingModule ); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b0c16baf572..1c6745438ad 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -77,7 +77,7 @@ contract ProposalValidator_Init is CommonTest { bytes[] calldatas, string description, ProposalValidator.ProposalType proposalType, - uint8 proposalTypeConfigurator + uint8 proposalVotingModule ); event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); @@ -87,7 +87,7 @@ contract ProposalValidator_Init is CommonTest { ); event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalTypeDataSet( - ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalTypeConfigurator + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); /// @notice Helper function to setup a mock and expect a call to it. @@ -126,23 +126,23 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); return (proposalTypes, proposalTypesData); @@ -252,7 +252,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); @@ -266,7 +266,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { _calldatas, _description, proposalType, - proposalTypeConfigurator + proposalVotingModule ); // Submit the proposal @@ -286,7 +286,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID vm.prank(topDelegate_A); @@ -299,7 +299,7 @@ contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; // Create attestation with wrong delegate bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); @@ -320,7 +320,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -360,7 +360,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -388,7 +388,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; function setUp() public override { super.setUp(); @@ -396,7 +396,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypeConfigurator = 0; + proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -411,9 +411,7 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { function test_moveToVote_succeeds() public { _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -436,7 +434,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { bytes[] calldatas; string description; ProposalValidator.ProposalType proposalType; - uint8 proposalTypeConfigurator; + uint8 proposalVotingModule; function setUp() public override { super.setUp(); @@ -444,7 +442,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { (targets, values, calldatas, description) = _createProposalSetup(); proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypeConfigurator = 0; + proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); vm.prank(topDelegate_A); @@ -471,9 +469,7 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -588,7 +584,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalTypeConfigurator: newConfigurator + proposalVotingModule: newConfigurator }); // Expect the ProposalTypeDataSet event to be emitted @@ -598,14 +594,14 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setProposalTypeData(proposalType, newData); - (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalType); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalTypeConfigurator, newConfigurator); + assertEq(proposalVotingModule, newConfigurator); } function test_setProposalTypeData_notOwner_reverts() public { ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalTypeConfigurator: 0 }); + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); vm.prank(rando); vm.expectRevert("Ownable: caller is not the owner"); @@ -622,7 +618,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { _createProposalSetup(); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalTypeConfigurator = 0; + uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); // Expect ProposalSubmitted event @@ -636,7 +632,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { calldatas, description, proposalType, - proposalTypeConfigurator + proposalVotingModule ); vm.prank(topDelegate_A); @@ -663,9 +659,7 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { // Mock the governor call _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.propose, (targets, values, calldatas, description, proposalTypeConfigurator) - ), + abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), abi.encode(1) ); @@ -732,9 +726,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalTypeConfigurator) = validator.proposalTypesData(proposalTypes[i]); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalTypeConfigurator, 0); + assertEq(proposalVotingModule, 0); } } @@ -748,11 +742,11 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalTypeConfigurator: 0 + proposalVotingModule: 0 }); vm.prank(owner); From ce51b0cebf4c2c2ed95e3bcb3132ff4580a7e523 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:13:25 -0300 Subject: [PATCH 42/73] feat: add hashProposalWithModule function (#403) * feat: add hashProposalWithModule function * chore: run pre-pr * test: add hashProposalWithModule tests * refactor: remove hashProposal function * chore: run pre-pr --- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 24 +++++---- .../test/governance/ProposalValidator.t.sol | 51 ++++++++++++++++--- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2e38b0ebd96..dd4a332b674 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x44d71e84d08aeb7abc82993a0293f646952d83e439e95427d2c432568f95524e", - "sourceCodeHash": "0xddf3e6506f0155d0120e467cb1437c49b1eff28d362b8e7de55101cd91e43427" + "initCodeHash": "0x553e9a5abda992f985f23d02f07c169be7ee39063d6dbd00742b4298089d3602", + "sourceCodeHash": "0xe3649d1d6a51572d2f6a0f82683d54eb3717dae3373f9a0c2a3648c392e66433" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index dcb729480be..18ef4316389 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -267,7 +267,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { { _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - proposalHash_ = _hashProposal(_targets, _values, _calldatas, _description); + proposalHash_ = bytes32(0); // TODO: Implement hashProposalWithModule ProposalData storage proposal = _proposals[proposalHash_]; if (proposal.proposer != address(0)) { @@ -327,7 +327,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (uint256 governorProposalId_) { // Verify that the provided data matches the proposalHash - bytes32 _proposalHash = _hashProposal(_targets, _values, _calldatas, _description); + bytes32 _proposalHash = bytes32(0); // TODO: Implement hashProposalWithModule ProposalData storage proposal = _proposals[_proposalHash]; @@ -454,17 +454,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); } - function _hashProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. + /// @param _module The address of the voting module to use for this proposal. + /// @param _proposalData The proposal data to pass to the voting module. + /// @param _descriptionHash The hash of the proposal description. + /// @return The hash of the proposal. + function _hashProposalWithModule( + address _module, + bytes memory _proposalData, + bytes32 _descriptionHash ) internal - pure - returns (bytes32 proposalHash_) + view + returns (bytes32) { - return keccak256(abi.encode(_targets, _values, _calldatas, _description)); + return keccak256(abi.encode(address(this), _module, _proposalData, _descriptionHash)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 1c6745438ad..3b6c8f36973 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -30,17 +30,16 @@ contract ProposalValidatorForTest is ProposalValidator { ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) { } - function hashProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + function hashProposalWithModule( + address _module, + bytes memory _proposalData, + bytes32 _descriptionHash ) public - pure + view returns (bytes32) { - return _hashProposal(_targets, _values, _calldatas, _description); + return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } } @@ -254,7 +253,7 @@ contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - bytes32 expectedProposalHash = validator.hashProposal(_targets, _values, _calldatas, _description); + bytes32 expectedProposalHash = bytes32(0); // TODO: Implement hashProposalWithModule // Expect event to be emitted vm.expectEmit(address(validator)); @@ -678,6 +677,42 @@ contract ProposalValidator_Integration_Test is ProposalValidator_Init { } } +/// @title ProposalValidator_HashProposalWithModule_Test +/// @notice Tests for the hashProposalWithModule function +contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { + function test_hashProposalWithModule_succeeds() public { + address testModule = makeAddr("testModule"); + bytes memory testProposalData = abi.encode("test", "proposal", "data"); + bytes32 testDescriptionHash = keccak256("test description"); + + bytes32 hash = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + assertTrue(hash != bytes32(0)); + } + + function test_hashProposalWithModule_consistentHash_succeeds() public { + address testModule = makeAddr("testModule"); + bytes memory testProposalData = abi.encode("test data"); + bytes32 testDescriptionHash = keccak256("description"); + + bytes32 hash1 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + bytes32 hash2 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); + + assertEq(hash1, hash2); + } + + function test_hashProposalWithModule_differentInputs_succeeds() public { + address module1 = makeAddr("module1"); + address module2 = makeAddr("module2"); + bytes memory data = abi.encode("data"); + bytes32 descHash = keccak256("desc"); + + bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); + bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); + + assertTrue(hash1 != hash2); + } +} + /// @title ProposalValidator_Initialize_Test /// @notice Tests for the initialize function contract ProposalValidator_Initialize_Test is ProposalValidator_Init { From 525f45e8fd98dc2ef4931dc992480ee521b4917c Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:54:22 -0300 Subject: [PATCH 43/73] chore: remove submit proposal (#409) * chore: remove submitProposal function * test: remove usage of submitProposal function --- .../governance/IProposalValidator.sol | 20 --- .../snapshots/abi/ProposalValidator.json | 99 ------------ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 128 ++------------- .../test/governance/ProposalValidator.t.sol | 151 +----------------- 5 files changed, 24 insertions(+), 378 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7ab9b4194a5..123aa8421fd 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -40,17 +40,6 @@ interface IProposalValidator is ISemver { CouncilBudget } - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, - string description, - ProposalType proposalType, - uint8 proposalVotingModule - ); - event ProposalApproved( bytes32 indexed proposalHash, address indexed approver @@ -78,15 +67,6 @@ interface IProposalValidator is ISemver { event Initialized(uint8 version); - function submitProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description, - ProposalType _proposalType, - bytes32 _attestationUid - ) external returns (bytes32 proposalHash_); - function approveProposal(bytes32 _proposalHash) external; function moveToVote( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index acb1665cd84..aa92ed7bf73 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -357,50 +357,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_values", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "_calldatas", - "type": "bytes[]" - }, - { - "internalType": "string", - "name": "_description", - "type": "string" - }, - { - "internalType": "enum ProposalValidator.ProposalType", - "name": "_proposalType", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "_attestationUid", - "type": "bytes32" - } - ], - "name": "submitProposal", - "outputs": [ - { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -552,61 +508,6 @@ "name": "ProposalMovedToVote", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "proposer", - "type": "address" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "indexed": false, - "internalType": "bytes[]", - "name": "calldatas", - "type": "bytes[]" - }, - { - "indexed": false, - "internalType": "string", - "name": "description", - "type": "string" - }, - { - "indexed": false, - "internalType": "enum ProposalValidator.ProposalType", - "name": "proposalType", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "proposalVotingModule", - "type": "uint8" - } - ], - "name": "ProposalSubmitted", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index dd4a332b674..cc1c082a151 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x553e9a5abda992f985f23d02f07c169be7ee39063d6dbd00742b4298089d3602", - "sourceCodeHash": "0xe3649d1d6a51572d2f6a0f82683d54eb3717dae3373f9a0c2a3648c392e66433" + "initCodeHash": "0xdcec7b9d2e1d4a7c7849e0b06fe1142fd743fd5927edceb1fc22466bf139d33c", + "sourceCodeHash": "0xf9bed487efd2ee53ab19814bbc32af947a6cb23f891b0a9af9059077155b64dd" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 18ef4316389..f5c28ab509e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -106,26 +106,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { EVENTS //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new proposal is submitted to the validator contract. - /// @param proposalHash The hash of the submitted proposal. - /// @param proposer The address that submitted the proposal. - /// @param targets Target addresses for proposal calls. - /// @param values ETH values for proposal calls. - /// @param calldatas Function data for proposal calls. - /// @param description Description of the proposal. - /// @param proposalType Type of the proposal. - /// @param proposalVotingModule Voting module specific to the proposal type. - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, - string description, - ProposalType proposalType, - uint8 proposalVotingModule - ); - /// @notice Emitted when a delegate approves a proposal. /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. @@ -247,51 +227,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } - /// @notice Submit a proposal for delegate approval - /// @param _targets Target addresses for proposal calls - /// @param _values ETH values for proposal calls - /// @param _calldatas Function data for proposal calls - /// @param _description Description of the proposal - /// @param _proposalType Type of the proposal - /// @return proposalHash_ The hash of the submitted proposal - function submitProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description, - ProposalType _proposalType, - bytes32 _attestationUid - ) - external - returns (bytes32 proposalHash_) - { - _validateProposal(_targets, _values, _calldatas, _proposalType, _attestationUid); - - proposalHash_ = bytes32(0); // TODO: Implement hashProposalWithModule - ProposalData storage proposal = _proposals[proposalHash_]; - - if (proposal.proposer != address(0)) { - revert ProposalValidator_ProposalAlreadySubmitted(); - } - - ProposalTypeData memory proposalTypeData = proposalTypesData[_proposalType]; - - proposal.proposer = msg.sender; - proposal.proposalType = _proposalType; - proposal.inVoting = false; - - emit ProposalSubmitted( - proposalHash_, - msg.sender, - _targets, - _values, - _calldatas, - _description, - _proposalType, - proposalTypeData.proposalVotingModule - ); - } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power) /// @param _proposalHash The hash of the proposal to approve function approveProposal(bytes32 _proposalHash) external { @@ -401,57 +336,22 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _setProposalTypeData(_proposalType, _proposalTypeData); } - /// @notice Validates a proposal before submission. - /// @dev Checks if the proposal requires approval and validates the attestation. - /// @param _targets Target addresses for proposal calls. - /// @param _values ETH values for proposal calls. - /// @param _calldatas Function data for proposal calls. - /// @param _proposalType Type of the proposal. - /// @param _attestationUid The UID of the attestation proving eligibility. - function _validateProposal( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - ProposalType _proposalType, - bytes32 _attestationUid - ) - private - view - { - if (_requiresAttestation(_proposalType)) { - Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); - if ( - attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID - || !_isValidAttestationData(attestation.data, _proposalType) - ) { - revert ProposalValidator_InvalidAttestation(); - } - } - } - - /// @notice Determines if a proposal type requires approval via attestation. - /// @param _proposalType The type of proposal to check. - /// @return requiresAttestation_ True if the proposal type requires approval, false otherwise. - function _requiresAttestation(ProposalType _proposalType) private pure returns (bool requiresAttestation_) { - return _proposalType == ProposalType.ProtocolOrGovernorUpgrade - || _proposalType == ProposalType.MaintenanceUpgrade || _proposalType == ProposalType.CouncilMemberElections; - } - /// @notice Validates the attestation data for a proposal. - /// @dev Checks that the sender is the approved delegate and that the proposal type is correct. - /// @param _data The attestation data to validate. + /// @dev Checks that the attester is the owner, the schema is correct, + /// the sender is the approved delegate, and that the proposal type is correct. + /// Reverts with ProposalValidator_InvalidAttestation if validation fails. + /// @param _attestationUid The UID of the attestation to validate. /// @param _expectedProposalType The expected proposal type from the attestation. - /// @return isValid_ True if the attestation data is valid, false otherwise. - function _isValidAttestationData( - bytes memory _data, - ProposalType _expectedProposalType - ) - private - view - returns (bool isValid_) - { - (address approvedDelegate, uint8 proposalType) = abi.decode(_data, (address, uint8)); - isValid_ = approvedDelegate == msg.sender && proposalType == uint8(_expectedProposalType); + function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); + + if ( + attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) + ) { + revert ProposalValidator_InvalidAttestation(); + } } /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 3b6c8f36973..5d2afb8a99a 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -243,72 +243,6 @@ contract ProposalValidator_Init is CommonTest { } } -/// @title ProposalValidator_SubmitProposal_Test -/// @notice Happy path tests for submitProposal function -contract ProposalValidator_SubmitProposal_Test is ProposalValidator_Init { - function test_submitProposal_succeeds() public { - (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - bytes32 expectedProposalHash = bytes32(0); // TODO: Implement hashProposalWithModule - - // Expect event to be emitted - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedProposalHash, - topDelegate_A, - _targets, - _values, - _calldatas, - _description, - proposalType, - proposalVotingModule - ); - - // Submit the proposal - vm.prank(topDelegate_A); - bytes32 proposalHash = - validator.submitProposal(_targets, _values, _calldatas, _description, proposalType, attestationUid); - - assertEq(proposalHash, expectedProposalHash); - } -} - -/// @title ProposalValidator_SubmitProposal_TestFail -/// @notice Sad path tests for submitProposal function -contract ProposalValidator_SubmitProposal_TestFail is ProposalValidator_Init { - function test_submitProposal_invalidAttestation_reverts() public { - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 invalidAttestationUid = bytes32(uint256(1)); // Invalid attestation UID - - vm.prank(topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, invalidAttestationUid); - } - - function test_submitProposal_wrongAttester_reverts() public { - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - - // Create attestation with wrong delegate - bytes32 attestationUid = _createAttestation(topDelegate_B, proposalType); - - vm.prank(topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - } -} - /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { @@ -322,8 +256,8 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_approveProposal_succeeds() public { @@ -362,8 +296,8 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { uint8 proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_approveProposal_insufficientVotingPower_reverts() public { @@ -398,8 +332,8 @@ contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented _approveProposal(topDelegate_A, proposalHash); _approveProposal(topDelegate_B, proposalHash); @@ -444,8 +378,8 @@ contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { proposalVotingModule = 0; bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - vm.prank(topDelegate_A); - proposalHash = validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); + /* vm.prank(topDelegate_A); */ + proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } function test_moveToVote_insufficientApprovals_reverts() public { @@ -608,75 +542,6 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } } -/// @title ProposalValidator_Integration_Test -/// @notice Integration tests for the full proposal flow -contract ProposalValidator_Integration_Test is ProposalValidator_Init { - function test_proposalFullFlow_succeeds() public { - // Create a proposal - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - - // Expect ProposalSubmitted event - bytes32 expectedProposalHash = keccak256(abi.encode(targets, values, calldatas, description)); - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedProposalHash, - topDelegate_A, - targets, - values, - calldatas, - description, - proposalType, - proposalVotingModule - ); - - vm.prank(topDelegate_A); - bytes32 proposalHash = - validator.submitProposal(targets, values, calldatas, description, proposalType, attestationUid); - - // Expect ProposalApproved events for each approval - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_A); - _approveProposal(topDelegate_A, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_B); - _approveProposal(topDelegate_B, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_C); - _approveProposal(topDelegate_C, proposalHash); - - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_D); - _approveProposal(topDelegate_D, proposalHash); - - // Mock the governor call - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); - - // Expect ProposalMovedToVote event - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(proposalHash, owner); - - // Move to vote phase - vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); - - // It reverts when proposal is already in voting phase - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); - } -} - /// @title ProposalValidator_HashProposalWithModule_Test /// @notice Tests for the hashProposalWithModule function contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { From 1ade9875dbb0bad56cba8a1ab199589f4197d5a7 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:23:08 -0300 Subject: [PATCH 44/73] feat: add submit funding proposal (#411) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * fix: remove duplicated tests * perf: optimiza for loops usage * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr --- .semgrep/rules/sol-rules.yaml | 29 + .../governance/IOptimismGovernor.sol | 8 + .../governance/IProposalTypesConfigurator.sol | 50 ++ .../governance/IProposalValidator.sol | 40 +- .../snapshots/abi/ProposalValidator.json | 127 +++++ .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 17 +- .../src/governance/ApprovalVotingModule.sol | 522 ++++++++++++++++++ .../src/governance/ProposalValidator.sol | 150 ++++- .../test/governance/ProposalValidator.t.sol | 515 ++++++++++++++++- 10 files changed, 1430 insertions(+), 32 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol create mode 100644 packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index d77eb17a8a4..b5c7e67371d 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -45,7 +45,12 @@ rules: - pattern: vm.expectRevert() paths: exclude: +<<<<<<< HEAD - packages/contracts-bedrock/test/universal/WETH98.t.sol +======= + - packages/contracts-bedrock/test/dispute/WETH98.t.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol +>>>>>>> feat: add submit funding proposal (#411) - id: sol-safety-natspec-semver-match languages: [generic] @@ -110,6 +115,7 @@ rules: exclude: - packages/contracts-bedrock/test - packages/contracts-bedrock/scripts + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-input-arg-fmt languages: [solidity] @@ -126,6 +132,7 @@ rules: - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol - packages/contracts-bedrock/src/governance/GovernanceToken.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -141,6 +148,7 @@ rules: - packages/contracts-bedrock/scripts/libraries/Solarray.sol - packages/contracts-bedrock/scripts/interfaces/IGnosisSafe.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-doc-comment languages: [solidity] @@ -150,6 +158,7 @@ rules: paths: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -167,6 +176,14 @@ rules: exclude: - packages/contracts-bedrock/src/libraries/Bytes.sol - packages/contracts-bedrock/src/legacy/LegacyMintableERC20.sol +<<<<<<< HEAD +======= + - packages/contracts-bedrock/src/cannon/MIPS.sol + - packages/contracts-bedrock/src/cannon/MIPS2.sol + - packages/contracts-bedrock/src/cannon/libraries/MIPSMemory.sol + - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol +>>>>>>> feat: add submit funding proposal (#411) - id: sol-style-malformed-revert languages: [solidity] @@ -180,6 +197,13 @@ rules: - pattern-not-regex: string\.concat\(\"(\w+:\s[^"]*)\"\,.+\) - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\" - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\" +<<<<<<< HEAD +======= + paths: + exclude: + - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol +>>>>>>> feat: add submit funding proposal (#411) - id: sol-style-use-abi-encodecall languages: [solidity] @@ -195,6 +219,7 @@ rules: paths: exclude: - packages/contracts-bedrock/src/legacy/L1ChugSplashProxy.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-enforce-require-msg languages: [solidity] @@ -206,6 +231,7 @@ rules: paths: exclude: - packages/contracts-bedrock/src/universal/WETH98.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-no-bare-imports languages: [solidity] @@ -215,6 +241,7 @@ rules: paths: exclude: - packages/contracts-bedrock/test + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - id: sol-style-error-format languages: [generic] @@ -236,6 +263,7 @@ rules: - packages/contracts-bedrock/src/dispute/lib/Errors.sol - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/governance/VotingModule.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol @@ -337,6 +365,7 @@ rules: - packages/contracts-bedrock/src/governance/ProposalValidator.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ProposalValidator.sol + - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - packages/contracts-bedrock/src/periphery/TransferOnion.sol - packages/contracts-bedrock/src/periphery/faucet/Faucet.sol - packages/contracts-bedrock/src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index fd9e773b466..39dd12b664a 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {VotingModule} from "src/governance/VotingModule.sol"; + interface IOptimismGovernor { function propose( address[] memory targets, @@ -17,4 +18,11 @@ interface IOptimismGovernor { string memory description, uint8 proposalType ) external returns (uint256 proposalId); + + function timelock() external view returns (address); + + /// @notice Returns the snapshot block number for a proposal, 0 if proposal doesn't exist + /// @param proposalId The ID of the proposal + /// @return The snapshot block number, or 0 if proposal doesn't exist + function proposalSnapshot(uint256 proposalId) external view returns (uint256); } \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol new file mode 100644 index 00000000000..17e92cd5d5b --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IProposalTypesConfigurator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IProposalTypesConfigurator { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error InvalidQuorum(); + error InvalidApprovalThreshold(); + error NotManagerOrTimelock(); + error AlreadyInit(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event ProposalTypeSet( + uint8 indexed proposalTypeId, uint16 quorum, uint16 approvalThreshold, string name, string description + ); + + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + struct ProposalType { + uint16 quorum; + uint16 approvalThreshold; + string name; + string description; + address module; + } + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function initialize(address _governor, ProposalType[] calldata _proposalTypes) external; + + function proposalTypes(uint8 proposalTypeId) external view returns (ProposalType memory); + + function setProposalType( + uint8 proposalTypeId, + uint16 quorum, + uint16 approvalThreshold, + string memory name, + string memory description, + address module + ) external; +} diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 123aa8421fd..4bae770c23c 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import {IGovernanceToken} from './IGovernanceToken.sol'; import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. @@ -18,6 +19,9 @@ interface IProposalValidator is ISemver { error ProposalValidator_VotingCycleAlreadySet(); error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); + error ProposalValidator_InvalidFundingProposalType(); + error ProposalValidator_ExceedsDistributionThreshold(); + error ProposalValidator_InvalidOptionsLength(); struct ProposalData { address proposer; @@ -56,7 +60,16 @@ interface IProposalValidator is ISemver { event DistributionThresholdSet(uint256 newDistributionThreshold); - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + event ProposalTypeDataSet( + ProposalType proposalType, + uint256 requiredApprovals, + uint8 proposalVotingModule + ); + + event ProposalVotingModuleData( + bytes32 indexed proposalHash, + bytes encodedVotingModuleData + ); event VotingCycleDataSet( uint256 cycleNumber, @@ -64,6 +77,13 @@ interface IProposalValidator is ISemver { uint256 duration, uint256 votingCycleDistributionLimit ); + + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + string description, + ProposalType proposalType + ); event Initialized(uint8 version); @@ -79,7 +99,7 @@ interface IProposalValidator is ISemver { function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; - + function setProposalTypeData( ProposalType _proposalType, ProposalTypeData memory _proposalTypeData @@ -91,9 +111,19 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit ) external; - + + function submitFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) external returns (bytes32 proposalHash_); + function initialize( address _owner, + IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, @@ -123,9 +153,11 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); - + function votingCycles(uint256) external view returns ( uint256 startingBlock, uint256 duration, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index aa92ed7bf73..4f96acab934 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -124,6 +124,11 @@ "name": "_owner", "type": "address" }, + { + "internalType": "contract IProposalTypesConfigurator", + "name": "_proposalTypesConfigurator", + "type": "address" + }, { "internalType": "uint256", "name": "_minimumVotingPower", @@ -242,6 +247,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proposalTypesConfigurator", + "outputs": [ + { + "internalType": "contract IProposalTypesConfigurator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -357,6 +375,50 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, + { + "internalType": "address[]", + "name": "_optionsRecipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_optionsAmounts", + "type": "uint256[]" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "submitFundingProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -508,6 +570,37 @@ "name": "ProposalMovedToVote", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum ProposalValidator.ProposalType", + "name": "proposalType", + "type": "uint8" + } + ], + "name": "ProposalSubmitted", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -533,6 +626,25 @@ "name": "ProposalTypeDataSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "proposalHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "encodedVotingModuleData", + "type": "bytes" + } + ], + "name": "ProposalVotingModuleData", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -564,6 +676,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_ExceedsDistributionThreshold", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InsufficientApprovals", @@ -579,6 +696,16 @@ "name": "ProposalValidator_InvalidAttestation", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidFundingProposalType", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidOptionsLength", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index cc1c082a151..40fe972eec9 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xdcec7b9d2e1d4a7c7849e0b06fe1142fd743fd5927edceb1fc22466bf139d33c", - "sourceCodeHash": "0xf9bed487efd2ee53ab19814bbc32af947a6cb23f891b0a9af9059077155b64dd" + "initCodeHash": "0xa3010da5a7dd34d0256de17d400b9b39ded7339acbd33a2e609a2ef2b6140be5", + "sourceCodeHash": "0xbb7d2b4bb9b9789f27b585751da0092ac117615fab7019086c9af3ab0d43311f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 8b940980418..8e422ff0af2 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,39 +34,46 @@ "slot": "52", "type": "uint256[49]" }, + { + "bytes": "20", + "label": "proposalTypesConfigurator", + "offset": 0, + "slot": "101", + "type": "contract IProposalTypesConfigurator" + }, { "bytes": "32", "label": "minimumVotingPower", "offset": 0, - "slot": "101", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "102", + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "103", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "104", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "105", + "slot": "106", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol new file mode 100644 index 00000000000..7f189d3f6ec --- /dev/null +++ b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { EnumerableSetUpgradeable } from + "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { VotingModule } from "./VotingModule.sol"; + +enum VoteType { + Against, + For, + Abstain +} + +enum PassingCriteria { + Threshold, + TopChoices +} + +struct ExecuteParams { + address targets; + uint256 values; + bytes calldatas; +} + +struct ProposalSettings { + uint8 maxApprovals; + uint8 criteria; + address budgetToken; + uint128 criteriaValue; + uint128 budgetAmount; +} + +struct ProposalOption { + uint256 budgetTokensSpent; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; +} + +struct Proposal { + address governor; + uint256 initBalance; + uint128[] optionVotes; + ProposalOption[] options; + ProposalSettings settings; +} + +contract ApprovalVotingModule is VotingModule { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error WrongProposalId(); + error MaxChoicesExceeded(); + error MaxApprovalsExceeded(); + error BudgetExceeded(); + error OptionsNotStrictlyAscending(); + + /*////////////////////////////////////////////////////////////// + LIBRARIES + //////////////////////////////////////////////////////////////*/ + + using SafeCastLib for uint256; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => Proposal) public proposals; + mapping(uint256 => mapping(address => EnumerableSetUpgradeable.UintSet)) private accountVotesSet; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) VotingModule(_governor) { } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * Save settings and options for a new proposal. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. + */ + function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external override { + _onlyGovernor(); + if (proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), proposalData, descriptionHash)))) { + revert WrongProposalId(); + } + + if (proposals[proposalId].governor != address(0)) { + revert ExistingProposal(); + } + + (ProposalOption[] memory proposalOptions, ProposalSettings memory proposalSettings) = + abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + uint256 optionsLength = proposalOptions.length; + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert InvalidParams(); + } + if (proposalSettings.criteria == uint8(PassingCriteria.TopChoices)) { + if (proposalSettings.criteriaValue > optionsLength) { + revert MaxChoicesExceeded(); + } + } + + unchecked { + // Ensure proposal params of each option have the same length between themselves + ProposalOption memory option; + for (uint256 i; i < optionsLength; ++i) { + option = proposalOptions[i]; + if (option.targets.length != option.values.length || option.targets.length != option.calldatas.length) { + revert InvalidParams(); + } + + proposals[proposalId].options.push(option); + } + } + + proposals[proposalId].governor = msg.sender; + proposals[proposalId].settings = proposalSettings; + proposals[proposalId].optionVotes = new uint128[](optionsLength); + } + + /** + * Count approvals voted by `account`. If voting for, options need to be set in ascending order. Votes can only be + * cast once. + * + * @param proposalId The id of the proposal. + * @param account The account to count votes for. + * @param support The type of vote to count. + * @param weight The total vote weight of the `account`. + * @param params The ids of the options to vote for sorted in ascending order, encoded as `uint256[]`. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) + external + virtual + override + { + _onlyGovernor(); + Proposal memory proposal = proposals[proposalId]; + + if (support == uint8(VoteType.For)) { + if (weight != 0) { + uint256[] memory options = _decodeVoteParams(params); + uint256 totalOptions = options.length; + if (totalOptions == 0) revert InvalidParams(); + + _recordVote( + proposalId, account, weight.toUint128(), options, totalOptions, proposal.settings.maxApprovals + ); + } + } + } + + /** + * Format executeParams for a governor, given `proposalId` and `proposalData`. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. + * @return targets The targets of the proposal. + * @return values The values of the proposal. + * @return calldatas The calldatas of the proposal. + */ + function _formatExecuteParams( + uint256 proposalId, + bytes memory proposalData + ) + public + override + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) + { + _onlyGovernor(); + (ProposalOption[] memory options, ProposalSettings memory settings) = + abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + { + IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); + + // If budgetToken is not ETH + if (settings.budgetToken != address(0)) { + // Save initBalance to be used as comparison in `_afterExecute` + proposals[proposalId].initBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); + } + } + + (uint128[] memory sortedOptionVotes, ProposalOption[] memory sortedOptions) = + _sortOptions(proposals[proposalId].optionVotes, options); + + (uint256 executeParamsLength, uint256 succeededOptionsLength) = + _countOptions(sortedOptions, sortedOptionVotes, settings); + + ExecuteParams[] memory executeParams = new ExecuteParams[](executeParamsLength); + executeParamsLength = 0; + uint256 n; + uint256 totalValue; + ProposalOption memory option; + + { + bool budgetExceeded = false; + + // Flatten `options` by filling `executeParams` until budgetAmount is exceeded + for (uint256 i; i < succeededOptionsLength;) { + option = sortedOptions[i]; + + for (n = 0; n < option.targets.length;) { + // If `budgetToken` is ETH and value is not zero, add transaction value to `totalValue` + if (settings.budgetToken == address(0) && option.values[n] != 0) { + if (totalValue + option.values[n] > settings.budgetAmount) { + budgetExceeded = true; + break; // break inner loop + } + totalValue += option.values[n]; + } + + unchecked { + executeParams[executeParamsLength + n] = + ExecuteParams(option.targets[n], option.values[n], option.calldatas[n]); + + ++n; + } + } + + // If `budgetAmount` for ETH is exceeded, skip option. + if (budgetExceeded) break; + + // Check if budgetAmount is exceeded for non-ETH tokens + if (settings.budgetToken != address(0) && settings.budgetAmount != 0) { + if (option.budgetTokensSpent != 0) { + if (totalValue + option.budgetTokensSpent > settings.budgetAmount) break; // break outer loop + // for non-ETH tokens + totalValue += option.budgetTokensSpent; + } + } + + unchecked { + executeParamsLength += n; + + ++i; + } + } + } + + unchecked { + // Increase by one to account for additional `_afterExecute` call + uint256 effectiveParamsLength = executeParamsLength + 1; + + // Init params lengths + targets = new address[](effectiveParamsLength); + values = new uint256[](effectiveParamsLength); + calldatas = new bytes[](effectiveParamsLength); + } + + // Set n `targets`, `values` and `calldatas` + for (uint256 i; i < executeParamsLength;) { + targets[i] = executeParams[i].targets; + values[i] = executeParams[i].values; + calldatas[i] = executeParams[i].calldatas; + + unchecked { + ++i; + } + } + + // Set `_afterExecute` as last call + targets[executeParamsLength] = address(this); + values[executeParamsLength] = 0; + calldatas[executeParamsLength] = + abi.encodeWithSelector(this._afterExecute.selector, proposalId, proposalData, totalValue); + } + + /** + * Hook called by a governor after execute, for `proposalId` with `proposalData`. + * Revert if the transaction has resulted in more tokens being spent than `budgetAmount`. + * + * @param proposalId The id of the proposal. + * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. + * @param budgetTokensSpent The total amount of tokens that can be spent. + */ + function _afterExecute(uint256 proposalId, bytes memory proposalData, uint256 budgetTokensSpent) public view { + (, ProposalSettings memory settings) = abi.decode(proposalData, (ProposalOption[], ProposalSettings)); + + if (settings.budgetToken != address(0) && settings.budgetAmount > 0) { + IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); + + uint256 initBalance = proposals[proposalId].initBalance; + uint256 finalBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); + + // If `finalBalance` is higher than `initBalance`, ignore the budget check + if (finalBalance < initBalance) { + /// @dev Cannot underflow as `finalBalance` is less than `initBalance` + unchecked { + if (initBalance - finalBalance > budgetTokensSpent) { + revert BudgetExceeded(); + } + } + } + } + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * Return the ids of the options voted by `account` on `proposalId`. + */ + function getAccountVotes(uint256 proposalId, address account) external view returns (uint256[] memory) { + return accountVotesSet[proposalId][account].values(); + } + + /** + * Return the total number of votes cast by `account` on `proposalId`. + */ + function getAccountTotalVotes(uint256 proposalId, address account) external view returns (uint256) { + return accountVotesSet[proposalId][account].length(); + } + + /** + * @dev Return true if at least one option satisfies the passing criteria. + * Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. + * + * @param proposalId The id of the proposal. + */ + function _voteSucceeded(uint256 proposalId) external view override returns (bool) { + Proposal memory proposal = proposals[proposalId]; + + ProposalOption[] memory options = proposal.options; + uint256 n = options.length; + unchecked { + if (proposal.settings.criteria == uint8(PassingCriteria.Threshold)) { + for (uint256 i; i < n; ++i) { + if (proposal.optionVotes[i] >= proposal.settings.criteriaValue) return true; + } + } else if (proposal.settings.criteria == uint8(PassingCriteria.TopChoices)) { + for (uint256 i; i < n; ++i) { + if (proposal.optionVotes[i] != 0) return true; + } + } + } + + return false; + } + + /** + * Defines the encoding for the expected `proposalData` in `propose`. + * Encoding: `(ProposalOption[], ProposalSettings)` + * + * @dev Can be used by clients to interact with modules programmatically without prior knowledge + * on expected types. + */ + function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { + return + "((uint256 budgetTokensSpent,address[] targets,uint256[] values,bytes[] calldatas,string description)[] proposalOptions,(uint8 maxApprovals,uint8 criteria,address budgetToken,uint128 criteriaValue,uint128 budgetAmount) proposalSettings)"; + } + + /** + * Defines the encoding for the expected `params` in `_countVote`. + * Encoding: `uint256[]` + * + * @dev Can be used by clients to interact with modules programmatically without prior knowledge + * on expected types. + */ + function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { + return "uint256[] optionIds"; + } + + /** + * @dev See {IGovernor-COUNTING_MODE}. + * + * - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. + * - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. + * - `params=approvalVote`: params needs to be formatted as `VOTE_PARAMS_ENCODING`. + */ + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo&quorum=against,for,abstain¶ms=approvalVote"; + } + + /** + * Module version. + */ + function version() public pure returns (uint256) { + return 1; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _recordVote( + uint256 proposalId, + address account, + uint128 weight, + uint256[] memory options, + uint256 totalOptions, + uint256 maxApprovals + ) + internal + { + uint256 option; + uint256 prevOption; + for (uint256 i; i < totalOptions;) { + option = options[i]; + + accountVotesSet[proposalId][account].add(option); + + // Revert if `option` is not strictly ascending + if (i != 0) { + if (option <= prevOption) revert OptionsNotStrictlyAscending(); + } + + prevOption = option; + + /// @dev Revert if `option` is out of bounds + proposals[proposalId].optionVotes[option] += weight; + + unchecked { + ++i; + } + } + + if (accountVotesSet[proposalId][account].length() > maxApprovals) { + revert MaxApprovalsExceeded(); + } + } + + // Sort `options` by `optionVotes` in descending order + function _sortOptions( + uint128[] memory optionVotes, + ProposalOption[] memory options + ) + internal + pure + returns (uint128[] memory, ProposalOption[] memory) + { + unchecked { + uint128 highestValue; + ProposalOption memory highestOption; + uint256 index; + + for (uint256 i; i < optionVotes.length - 1; ++i) { + highestValue = optionVotes[i]; + + for (uint256 j = i + 1; j < optionVotes.length; ++j) { + if (optionVotes[j] > highestValue) { + highestValue = optionVotes[j]; + index = j; + } + } + + if (index != 0) { + optionVotes[index] = optionVotes[i]; + optionVotes[i] = highestValue; + + highestOption = options[index]; + options[index] = options[i]; + options[i] = highestOption; + + index = 0; + } + } + + return (optionVotes, options); + } + } + + // Derive `executeParamsLength` and `succeededOptionsLength` based on passing criteria + function _countOptions( + ProposalOption[] memory options, + uint128[] memory optionVotes, + ProposalSettings memory settings + ) + internal + pure + returns (uint256 executeParamsLength, uint256 succeededOptionsLength) + { + uint256 n = options.length; + unchecked { + uint256 i; + if (settings.criteria == uint8(PassingCriteria.Threshold)) { + // if criteria is `Threshold`, loop through options until `optionVotes` is less than threshold + for (i; i < n; ++i) { + if (optionVotes[i] >= settings.criteriaValue) { + executeParamsLength += options[i].targets.length; + } else { + break; + } + } + } else if (settings.criteria == uint8(PassingCriteria.TopChoices)) { + // if criteria is `TopChoices`, loop through options until the top choices are filled + for (i; i < settings.criteriaValue; ++i) { + if (optionVotes[i] > 0) { + executeParamsLength += options[i].targets.length; + } else { + break; + } + } + } + succeededOptionsLength = i; + } + } + + // Virtual method used to decode _countVote params. + function _decodeVoteParams(bytes memory params) internal virtual returns (uint256[] memory options) { + options = abi.decode(params, (uint256[])); + } +} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f5c28ab509e..a74046a468e 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -11,9 +11,14 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +// Modules +import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; + /// @custom:proxied true /// @title ProposalValidator /// @notice The ProposalValidator contract is responsible for validating proposals and moving @@ -47,6 +52,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the length of the proposal types and proposal types data arrays do not match. error ProposalValidator_ProposalTypesDataLengthMismatch(); + /// @notice Thrown when the proposal type is not valid for funding proposals. + error ProposalValidator_InvalidFundingProposalType(); + + /// @notice Thrown when the requested amount exceeds the distribution threshold. + error ProposalValidator_ExceedsDistributionThreshold(); + + /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). + error ProposalValidator_InvalidOptionsLength(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -68,7 +82,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing explicit data for each proposal type. /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. - /// @param proposalVotingModule The voting module each proposal type must use. + /// @param proposalVotingModule The proposal type ID used to get the voting module from the configurator. struct ProposalTypeData { uint256 requiredApprovals; uint8 proposalVotingModule; @@ -106,6 +120,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { EVENTS //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when a new proposal is submitted. + /// @param proposalHash The hash of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + ); + /// @notice Emitted when a delegate approves a proposal. /// @param proposalHash The hash of the approved proposal. /// @param approver The address of the delegate who approved the proposal. @@ -136,9 +159,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal voting module. + /// @param proposalVotingModule The proposal type ID. event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + /// @notice Emitted with ProposalSubmitted event. + /// @param proposalHash The hash of the submitted proposal. + /// @param encodedVotingModuleData The encoded voting module data. + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + /// @notice The schema UID for attestations in the Ethereum Attestation Service. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } bytes32 public immutable ATTESTATION_SCHEMA_UID; @@ -149,6 +177,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The token used to determine voting power. IGovernanceToken public immutable VOTING_TOKEN; + /// @notice The proposal types configurator contract. + IProposalTypesConfigurator public proposalTypesConfigurator; + /// @notice The minimum voting power required for a delegate to approve proposals. uint256 public minimumVotingPower; @@ -162,7 +193,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { mapping(ProposalType => ProposalTypeData) public proposalTypesData; /// @notice Mapping of proposal hash to their corresponding proposal data. - mapping(bytes32 => ProposalData) private _proposals; + mapping(bytes32 => ProposalData) internal _proposals; /// @notice Semantic version. /// @custom:semver 1.0.0-beta.1 @@ -189,6 +220,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. + /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _cycleNumber The number of the current voting cycle. /// @param _startBlock The block number of the starting block of the voting cycle. @@ -199,6 +231,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, + IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, @@ -215,6 +248,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } + proposalTypesConfigurator = _proposalTypesConfigurator; _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); @@ -227,6 +261,112 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and + /// voting. + /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), + /// then convert to the absolute vote count by calculating: (percentage / 100) * total_votable_supply. + /// Direct contract callers must provide the absolute number of votes required for passage. + /// @param _criteriaValue The absolute number of votes required for the proposal to pass. This represents the + /// threshold that must be met or exceeded for any option to be considered successful. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _optionsRecipients An address for each option to transfer funds to in case the option passes the voting. + /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. + /// @param _description Description of the proposal. + /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @return proposalHash_ The hash of the submitted proposal. + function submitFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + // Only funding proposal types can use this function + if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { + revert ProposalValidator_InvalidFundingProposalType(); + } + + // Validate input arrays have matching lengths + uint256 optionsLength = _optionsDescriptions.length; + if (optionsLength != _optionsRecipients.length || optionsLength != _optionsAmounts.length) { + revert ProposalValidator_ProposalTypesDataLengthMismatch(); + } + + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + + ProposalOption[] memory options = new ProposalOption[](optionsLength); + uint256 totalBudget = 0; + + // Check amounts, build options, and calculate total budget in single loop + for (uint256 i = 0; i < optionsLength; i++) { + if (_optionsAmounts[i] > distributionThreshold) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (_optionsRecipients[i], _optionsAmounts[i])); + + options[i] = ProposalOption({ + budgetTokensSpent: _optionsAmounts[i], + targets: targets, + values: values, + calldatas: calldatas, + description: _optionsDescriptions[i] + }); + + totalBudget += _optionsAmounts[i]; + } + + // Configure approval voting settings + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: _criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals with same hash + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = _proposalType; + proposal.inVoting = false; + + emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + } + /// @notice Approve a proposal (only callable by delegates with sufficient voting power) /// @param _proposalHash The hash of the proposal to approve function approveProposal(bytes32 _proposalHash) external { @@ -282,7 +422,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.inVoting = true; governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, proposalTypeData.proposalVotingModule); + GOVERNOR.propose(_targets, _values, _calldatas, _description, uint8(proposal.proposalType)); emit ProposalMovedToVote(_proposalHash, msg.sender); } @@ -368,7 +508,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { view returns (bytes32) { - return keccak256(abi.encode(address(this), _module, _proposalData, _descriptionHash)); + return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); } /// @notice Private function to set the minimum voting power and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 5d2afb8a99a..2731ecb6442 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -5,9 +5,11 @@ pragma solidity 0.8.15; import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; @@ -16,7 +18,11 @@ import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +// Modules +import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; + // Testing utilities +import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest @@ -41,11 +47,22 @@ contract ProposalValidatorForTest is ProposalValidator { { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } + + function getProposalData(bytes32 _proposalHash) + public + view + returns (address proposer_, ProposalType proposalType_, bool inVoting_, uint256 approvalCount_) + { + ProposalData storage proposal = _proposals[_proposalHash]; + return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); + } } /// @title ProposalValidator_Init /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_Init is CommonTest { + using stdStorage for StdStorage; + uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; @@ -61,22 +78,20 @@ contract ProposalValidator_Init is CommonTest { address topDelegate_B; address topDelegate_C; address topDelegate_D; + address approvalVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; IOptimismGovernor public governor; + IProposalTypesConfigurator public proposalTypesConfigurator; bytes32 public ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; event ProposalSubmitted( bytes32 indexed proposalHash, address indexed proposer, - address[] targets, - uint256[] values, - bytes[] calldatas, string description, - ProposalValidator.ProposalType proposalType, - uint8 proposalVotingModule + ProposalValidator.ProposalType proposalType ); event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); @@ -110,6 +125,50 @@ contract ProposalValidator_Init is CommonTest { validator.approveProposal(_proposalHash); } + /// @notice Helper function to set proposal type data using StdStorage. + function _setProposalTypeData( + ProposalValidator.ProposalType _proposalType, + ProposalValidator.ProposalTypeData memory _data + ) + internal + { + // Set requiredApprovals (depth 0) + stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(0) + .checked_write(_data.requiredApprovals); + + // Set proposalVotingModule (depth 1) + stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(1) + .checked_write(_data.proposalVotingModule); + } + + /// @notice Helper function to set GovernanceFund proposal type data. + function _setGovernanceFundProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.GovernanceFund, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 3 + }) + ); + } + + /// @notice Helper function to set CouncilBudget proposal type data. + function _setCouncilBudgetProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.CouncilBudget, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 4 + }) + ); + } + + /// @notice Helper function to set both funding proposal types. + function _setFundingProposalTypes() internal { + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); + } + function _getProposalTypesAndData() internal pure @@ -129,24 +188,94 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 1 }); proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 2 }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 3 }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 4 }); return (proposalTypes, proposalTypesData); } + function _constructVotingModuleData( + string[] memory descriptions, + address[] memory recipients, + uint256[] memory amounts, + uint128 criteriaValue + ) + internal + pure + returns (bytes memory) + { + // Construct ProposalOption array + ProposalOption[] memory options = new ProposalOption[](descriptions.length); + + for (uint256 i = 0; i < descriptions.length; i++) { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i])); + + options[i] = ProposalOption({ + budgetTokensSpent: amounts[i], + targets: targets, + values: values, + calldatas: calldatas, + description: descriptions[i] + }); + } + + // Calculate total budget + uint256 totalBudget = 0; + for (uint256 i = 0; i < amounts.length; i++) { + totalBudget += amounts[i]; + } + + // Construct ProposalSettings + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(descriptions.length), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + return abi.encode(options, settings); + } + + /// @notice Helper function to setup proposal types configurator mocks + function _setupProposalTypesConfiguratorMocks() internal { + // Mock calls for different proposal type IDs + for (uint8 i = 0; i < 5; i++) { + address moduleAddress = (i == 3 || i == 4) ? approvalVotingModule : address(0); + + vm.mockCall( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (i)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 100, + approvalThreshold: 100, + name: "Test Proposal Type", + description: "Test Description", + module: moduleAddress + }) + ) + ); + } + } + /// @notice Initializes the validator function _initializeValidator() internal virtual { ( @@ -154,8 +283,13 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalTypeData[] memory proposalTypesData ) = _getProposalTypesAndData(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); + + // Setup mocks + _setupProposalTypesConfiguratorMocks(); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -165,6 +299,7 @@ contract ProposalValidator_Init is CommonTest { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, @@ -184,6 +319,7 @@ contract ProposalValidator_Init is CommonTest { owner = governanceToken.owner(); rando = makeAddr("rando"); governor = IOptimismGovernor(makeAddr("governor")); + approvalVotingModule = makeAddr("approvalVotingModule"); vm.prank(owner); ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( @@ -507,7 +643,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setProposalTypeData_succeeds( uint8 proposalTypeValue, uint256 newRequiredApprovals, - uint8 newConfigurator + uint8 newProposalTypeId ) public { @@ -517,19 +653,19 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalVotingModule: newConfigurator + proposalVotingModule: newProposalTypeId }); // Expect the ProposalTypeDataSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newConfigurator); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); vm.prank(owner); validator.setProposalTypeData(proposalType, newData); (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newConfigurator); + assertEq(proposalVotingModule, newProposalTypeId); } function test_setProposalTypeData_notOwner_reverts() public { @@ -578,11 +714,356 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init } } +/// @title ProposalValidator_SubmitFundingProposal_Test +/// @notice Happy path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + string description; + + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + + function setUp() public override { + super.setUp(); + + _setFundingProposalTypes(); + + criteriaValue = 1000 ether; + } + + function testFuzz_submitFundingProposal_succeeds( + uint8 proposalTypeValue, + uint8 optionCount, + uint256 amount, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Bound option count between 1 and 50 for reasonable test execution + optionCount = uint8(bound(optionCount, 1, 50)); + + // Bound amount from 0 to DISTRIBUTION_THRESHOLD (inclusive) + amount = bound(amount, 0, DISTRIBUTION_THRESHOLD); + + // Create arrays based on option count + string[] memory descriptions = new string[](optionCount); + address[] memory recipients = new address[](optionCount); + uint256[] memory amounts = new uint256[](optionCount); + + for (uint256 i = 0; i < optionCount; i++) { + descriptions[i] = string(abi.encodePacked("Option ", vm.toString(i))); + recipients[i] = makeAddr(string(abi.encodePacked("recipient", vm.toString(i)))); + amounts[i] = amount; // Use the same bounded amount for all options + } + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructVotingModuleData(descriptions, recipients, amounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Expect ProposalSubmitted event + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, description, proposalType); + + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(inVoting, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitFundingProposal_TestFail +/// @notice Sad path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + string description; + + function setUp() public override { + super.setUp(); + + // Set GovernanceFund to use the approval voting module + _setGovernanceFundProposalType(); + + criteriaValue = 50; + optionsDescriptions = new string[](2); + optionsDescriptions[0] = "Option A"; + optionsDescriptions[1] = "Option B"; + + optionsRecipients = new address[](2); + optionsRecipients[0] = makeAddr("recipient1"); + optionsRecipients[1] = makeAddr("recipient2"); + + optionsAmounts = new uint256[](2); + optionsAmounts[0] = 1000 ether; + optionsAmounts[1] = 500 ether; + + description = "Test funding proposal"; + } + + function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Bound to proposal types that are NOT funding proposals (0, 1, 2) + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, description, proposalType + ); + } + + function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - recipients and amounts match, descriptions are different + string[] memory mismatchedDescriptions = new string[](mismatchedLength); + address[] memory matchingRecipients = new address[](matchingLength); + uint256[] memory matchingAmounts = new uint256[](matchingLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + mismatchedDescriptions, + matchingRecipients, + matchingAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_mismatchedRecipientsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - descriptions and amounts match, recipients are different + string[] memory matchingDescriptions = new string[](matchingLength); + address[] memory mismatchedRecipients = new address[](mismatchedLength); + uint256[] memory matchingAmounts = new uint256[](matchingLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + matchingDescriptions, + mismatchedRecipients, + matchingAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_mismatchedAmountsLength_reverts( + uint8 matchingLength, + uint8 mismatchedLength + ) + public + { + // Bound lengths to reasonable values (1-50) and ensure they're different + matchingLength = uint8(bound(matchingLength, 1, 50)); + mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); + vm.assume(matchingLength != mismatchedLength); + + // Create arrays - descriptions and recipients match, amounts are different + string[] memory matchingDescriptions = new string[](matchingLength); + address[] memory matchingRecipients = new address[](matchingLength); + uint256[] memory mismatchedAmounts = new uint256[](mismatchedLength); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + matchingDescriptions, + matchingRecipients, + mismatchedAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts(uint256 excessAmount) public { + // Bound excess amount to be greater than DISTRIBUTION_THRESHOLD + excessAmount = bound(excessAmount, DISTRIBUTION_THRESHOLD + 1, type(uint128).max); + + // Set first option to exceed the threshold + optionsAmounts[0] = excessAmount; + + vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = + _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Submit first proposal + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + + // Attempt to submit identical proposal + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = + _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + bytes32 expectedHash = + validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_zeroOptionsLength_reverts() public { + string[] memory emptyDescriptions = new string[](0); + address[] memory emptyRecipients = new address[](0); + uint256[] memory emptyAmounts = new uint256[](0); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + emptyDescriptions, + emptyRecipients, + emptyAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } + + function test_submitFundingProposal_exceedsMaxOptionsLength_reverts() public { + // Create arrays with 256 options (exceeds uint8 max of 255) + uint256 tooManyOptions = 256; + string[] memory tooManyDescriptions = new string[](tooManyOptions); + address[] memory tooManyRecipients = new address[](tooManyOptions); + uint256[] memory tooManyAmounts = new uint256[](tooManyOptions); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(rando); + validator.submitFundingProposal( + criteriaValue, + tooManyDescriptions, + tooManyRecipients, + tooManyAmounts, + description, + ProposalValidator.ProposalType.GovernanceFund + ); + } +} + /// @title ProposalValidator_Initialize_Test /// @notice Tests for the initialize function contract ProposalValidator_Initialize_Test is ProposalValidator_Init { /// @dev Override to create validator proxy without initialization for testing function _initializeValidator() internal override { + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); + + // Setup mocks + _setupProposalTypesConfiguratorMocks(); + impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); validator = ProposalValidatorForTest(address(new Proxy(owner))); // Initialize will be tested manually @@ -601,6 +1082,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, @@ -628,7 +1110,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalVotingModule, 0); + assertEq(proposalVotingModule, uint8(i)); } } @@ -646,7 +1128,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { }); proposalTypesData[1] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: 1 }); vm.prank(owner); @@ -657,6 +1139,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, + proposalTypesConfigurator, MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, From 6feee1b7cfa385a379370ad46879fa5220085585 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:58:44 -0300 Subject: [PATCH 45/73] fix: funding proposal comments (#425) * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr --- .../test/governance/ProposalValidator.t.sol | 239 ++++++++++-------- 1 file changed, 135 insertions(+), 104 deletions(-) diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2731ecb6442..2db50f2c3a6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -71,9 +71,10 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + uint8 public constant FUNDING_PROPOSALS_VOTING_MODULE = 3; address owner; - address rando; + address user; address topDelegate_A; address topDelegate_B; address topDelegate_C; @@ -147,7 +148,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 3 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }) ); } @@ -158,7 +159,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 4 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }) ); } @@ -169,6 +170,22 @@ contract ProposalValidator_Init is CommonTest { _setCouncilBudgetProposalType(); } + /// @notice Helper to create minimal valid arrays for funding proposal error tests + function _createMinimalFundingArrays() + internal + pure + returns (string[] memory descriptions_, address[] memory recipients_, uint256[] memory amounts_) + { + descriptions_ = new string[](1); + descriptions_[0] = "Option A"; + + recipients_ = new address[](1); + recipients_[0] = address(0x1); + + amounts_ = new uint256[](1); + amounts_[0] = 100 ether; + } + function _getProposalTypesAndData() internal pure @@ -196,11 +213,11 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 3 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 4 + proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE }); return (proposalTypes, proposalTypesData); @@ -258,7 +275,7 @@ contract ProposalValidator_Init is CommonTest { function _setupProposalTypesConfiguratorMocks() internal { // Mock calls for different proposal type IDs for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 3 || i == 4) ? approvalVotingModule : address(0); + address moduleAddress = (i == 2 || i == FUNDING_PROPOSALS_VOTING_MODULE) ? approvalVotingModule : address(0); vm.mockCall( address(proposalTypesConfigurator), @@ -317,7 +334,7 @@ contract ProposalValidator_Init is CommonTest { function setUp() public virtual override { super.setUp(); owner = governanceToken.owner(); - rando = makeAddr("rando"); + user = makeAddr("user"); governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); @@ -438,7 +455,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { function test_approveProposal_insufficientVotingPower_reverts() public { vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(rando, proposalHash); + _approveProposal(user, proposalHash); } function test_approveProposal_alreadyApproved_reverts() public { @@ -558,7 +575,7 @@ contract ProposalValidator_Getters_Test is ProposalValidator_Init { bool canSignOff = validator.canSignOff(topDelegate_A); assertTrue(canSignOff); - bool cannotSignOff = validator.canSignOff(rando); + bool cannotSignOff = validator.canSignOff(user); assertFalse(cannotSignOff); } } @@ -578,7 +595,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setMinimumVotingPower_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setMinimumVotingPower(10000 ether); } @@ -609,7 +626,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setVotingCycleData_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setVotingCycleData(2, block.number, 100, 10000 ether); } @@ -635,7 +652,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { } function test_setDistributionThreshold_notOwner_reverts() public { - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setDistributionThreshold(10000 ether); } @@ -672,7 +689,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - vm.prank(rando); + vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } @@ -808,32 +825,13 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init /// @title ProposalValidator_SubmitFundingProposal_TestFail /// @notice Sad path tests for submitFundingProposal function contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - string description; + uint128 public constant FUNDING_CRITERIA_VALUE = 50; + string description = "Test funding proposal"; function setUp() public override { super.setUp(); - - // Set GovernanceFund to use the approval voting module - _setGovernanceFundProposalType(); - - criteriaValue = 50; - optionsDescriptions = new string[](2); - optionsDescriptions[0] = "Option A"; - optionsDescriptions[1] = "Option B"; - - optionsRecipients = new address[](2); - optionsRecipients[0] = makeAddr("recipient1"); - optionsRecipients[1] = makeAddr("recipient2"); - - optionsAmounts = new uint256[](2); - optionsAmounts[0] = 1000 ether; - optionsAmounts[1] = 500 ether; - - description = "Test funding proposal"; + // Set both funding proposal types to use the approval voting module + _setFundingProposalTypes(); } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { @@ -842,16 +840,20 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, "Test proposal", proposalType ); } function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -860,26 +862,31 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - recipients and amounts match, descriptions are different string[] memory mismatchedDescriptions = new string[](mismatchedLength); address[] memory matchingRecipients = new address[](matchingLength); uint256[] memory matchingAmounts = new uint256[](matchingLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, mismatchedDescriptions, matchingRecipients, matchingAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } function testFuzz_submitFundingProposal_mismatchedRecipientsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -888,26 +895,31 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - descriptions and amounts match, recipients are different string[] memory matchingDescriptions = new string[](matchingLength); address[] memory mismatchedRecipients = new address[](mismatchedLength); uint256[] memory matchingAmounts = new uint256[](matchingLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, matchingDescriptions, mismatchedRecipients, matchingAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } function testFuzz_submitFundingProposal_mismatchedAmountsLength_reverts( uint8 matchingLength, - uint8 mismatchedLength + uint8 mismatchedLength, + uint8 proposalTypeValue ) public { @@ -916,46 +928,63 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedLength = uint8(bound(mismatchedLength, 1, 50)); vm.assume(matchingLength != mismatchedLength); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays - descriptions and recipients match, amounts are different string[] memory matchingDescriptions = new string[](matchingLength); address[] memory matchingRecipients = new address[](matchingLength); uint256[] memory mismatchedAmounts = new uint256[](mismatchedLength); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalTypesDataLengthMismatch.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, + FUNDING_CRITERIA_VALUE, matchingDescriptions, matchingRecipients, mismatchedAmounts, description, - ProposalValidator.ProposalType.GovernanceFund + proposalType ); } - function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts(uint256 excessAmount) public { + function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts( + uint256 excessAmount, + uint8 proposalTypeValue + ) + public + { // Bound excess amount to be greater than DISTRIBUTION_THRESHOLD excessAmount = bound(excessAmount, DISTRIBUTION_THRESHOLD + 1, type(uint128).max); - // Set first option to exceed the threshold - optionsAmounts[0] = excessAmount; + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Create arrays with excessive amount + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + amounts[0] = excessAmount; vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_duplicateProposal_reverts() public { + function testFuzz_submitFundingProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -967,33 +996,30 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); // Submit first proposal - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); // Attempt to submit identical proposal vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_proposalExistsInGovernor_reverts() public { + function testFuzz_submitFundingProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); + // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1005,50 +1031,46 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } - function test_submitFundingProposal_zeroOptionsLength_reverts() public { + function testFuzz_submitFundingProposal_zeroOptionsLength_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); string[] memory emptyDescriptions = new string[](0); address[] memory emptyRecipients = new address[](0); uint256[] memory emptyAmounts = new uint256[](0); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - emptyDescriptions, - emptyRecipients, - emptyAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, emptyDescriptions, emptyRecipients, emptyAmounts, description, proposalType ); } - function test_submitFundingProposal_exceedsMaxOptionsLength_reverts() public { - // Create arrays with 256 options (exceeds uint8 max of 255) - uint256 tooManyOptions = 256; + function testFuzz_submitFundingProposal_exceedsMaxOptionsLength_reverts( + uint256 tooManyOptions, + uint8 proposalTypeValue + ) + public + { + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Create arrays with more than 255 options (exceeds allowed uint8 max) + tooManyOptions = uint256(bound(tooManyOptions, 256, 512)); string[] memory tooManyDescriptions = new string[](tooManyOptions); address[] memory tooManyRecipients = new address[](tooManyOptions); uint256[] memory tooManyAmounts = new uint256[](tooManyOptions); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(rando); + vm.prank(user); validator.submitFundingProposal( - criteriaValue, - tooManyDescriptions, - tooManyRecipients, - tooManyAmounts, - description, - ProposalValidator.ProposalType.GovernanceFund + FUNDING_CRITERIA_VALUE, tooManyDescriptions, tooManyRecipients, tooManyAmounts, description, proposalType ); } } @@ -1110,7 +1132,16 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - assertEq(proposalVotingModule, uint8(i)); + + // Both GovernanceFund and CouncilBudget use FUNDING_PROPOSALS_VOTING_MODULE + if ( + proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + ) { + assertEq(proposalVotingModule, FUNDING_PROPOSALS_VOTING_MODULE); + } else { + assertEq(proposalVotingModule, uint8(i)); + } } } From 932b8c84057451e4c95da5b22b8c1f26425025ee Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:04:12 -0300 Subject: [PATCH 46/73] feat: add submitCouncilMemberElectionsProposal function (#418) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * feat: add submitCouncilMemberElectionsProposal function * chore: run pre-pr * fix: remove duplicated tests * test(fuzz): use fuzz testing for happy paths * feat: add check for criteria value < optionslength * perf: optimiza for loops usage * test: fuzz invalid attestationUid * test: fuzz invalid proposer * fix: lack of attestation existance validation * test: fuzz exceeded max options test * test: fuzz unapproved attester * test: reduce upper bound for array size * test: remove duplicated test * fix: update criteria value validation logic in ProposalValidator * refactor(test): use helper functions for duplicated logic * refactor: declare approvalVotingModule variable * test: increment the assertions in council memeber election tests * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr * refactor: use variables instead of hardcoded values * chore: rename voting module depending on configurator instead of internal proposal type * chore: improve tests naming --- .../governance/IProposalValidator.sol | 8 + .../snapshots/abi/ProposalValidator.json | 39 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 97 +++++- .../test/governance/ProposalValidator.t.sol | 301 +++++++++++++++++- 5 files changed, 436 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 4bae770c23c..aa787840981 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -22,6 +22,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); + error ProposalValidator_InvalidCriteriaValue(); struct ProposalData { address proposer; @@ -112,6 +113,13 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit ) external; + function submitCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionDescriptions, + string memory _proposalDescription, + bytes32 _attestationUid + ) external returns (bytes32 proposalHash_); + function submitFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 4f96acab934..ec85035250f 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -375,6 +375,40 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionDescriptions", + "type": "string[]" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + } + ], + "name": "submitCouncilMemberElectionsProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -696,6 +730,11 @@ "name": "ProposalValidator_InvalidAttestation", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidCriteriaValue", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidFundingProposalType", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 40fe972eec9..a04d1d919ce 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xa3010da5a7dd34d0256de17d400b9b39ded7339acbd33a2e609a2ef2b6140be5", - "sourceCodeHash": "0xbb7d2b4bb9b9789f27b585751da0092ac117615fab7019086c9af3ab0d43311f" + "initCodeHash": "0xe5a13d76f05b05db718ac645ee1aaf8770de307b8f4e367e3def458d1be6ba3f", + "sourceCodeHash": "0x2e2655c25888e6d3502374d7e257888ecc300188efbc18c44b19fc6640b00629" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index a74046a468e..91769149ae3 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -61,6 +61,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). error ProposalValidator_InvalidOptionsLength(); + /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). + error ProposalValidator_InvalidCriteriaValue(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -261,6 +264,93 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a Council Member Elections proposal for approval and voting. + /// @param _criteriaValue Since the passing criteria type is "TopChoices" this number represents the amount + /// of top choices that can pass the voting. + /// @param _optionDescriptions The strings of the different options that can be voted. + /// @param _proposalDescription Description of the proposal. + /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @return proposalHash_ The hash of the submitted proposal. + function submitCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionDescriptions, + string memory _proposalDescription, + bytes32 _attestationUid + ) + external + returns (bytes32 proposalHash_) + { + // Validate EAS attestation - must be called by owner-approved address + _validateAttestation(_attestationUid, ProposalType.CouncilMemberElections); + + // Validate options length bounds + uint256 optionsLength = _optionDescriptions.length; + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + + // Validate criteria value doesn't exceed options length for TopChoices + if (_criteriaValue > optionsLength) { + revert ProposalValidator_InvalidCriteriaValue(); + } + + ProposalOption[] memory options = new ProposalOption[](optionsLength); + + // Build proposal options without any execution calls (elections don't execute operations) + for (uint256 i = 0; i < optionsLength; i++) { + address[] memory targets = new address[](0); + uint256[] memory values = new uint256[](0); + bytes[] memory calldatas = new bytes[](0); + + options[i] = ProposalOption({ + budgetTokensSpent: 0, // No tokens spent for elections + targets: targets, + values: values, + calldatas: calldatas, + description: _optionDescriptions[i] + }); + } + + // Configure approval voting settings with TopChoices criteria + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), // No budget token for elections + criteriaValue: _criteriaValue, + budgetAmount: 0 // No budget amount for elections + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = proposalTypesConfigurator.proposalTypes( + proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule + ).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals with same hash + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = ProposalType.CouncilMemberElections; + + emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + } + /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and /// voting. /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), @@ -361,7 +451,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; - proposal.inVoting = false; emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -484,6 +573,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _expectedProposalType The expected proposal type from the attestation. function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + + // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) + if (attestation.uid == bytes32(0)) { + revert ProposalValidator_InvalidAttestation(); + } + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2db50f2c3a6..12593e24e7b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -11,6 +11,9 @@ import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// Testing utilities +import { stdStorage, StdStorage } from "forge-std/Test.sol"; + // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; import { Proxy } from "src/universal/Proxy.sol"; @@ -48,6 +51,7 @@ contract ProposalValidatorForTest is ProposalValidator { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } + /// @notice Exposes proposal data for testing function getProposalData(bytes32 _proposalHash) public view @@ -56,6 +60,11 @@ contract ProposalValidatorForTest is ProposalValidator { ProposalData storage proposal = _proposals[_proposalHash]; return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); } + + /// @notice Check if a delegate has approved a proposal + function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { + return _proposals[_proposalHash].delegateApprovals[_delegate]; + } } /// @title ProposalValidator_Init @@ -71,7 +80,7 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; - uint8 public constant FUNDING_PROPOSALS_VOTING_MODULE = 3; + uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; address owner; address user; @@ -142,13 +151,24 @@ contract ProposalValidator_Init is CommonTest { .checked_write(_data.proposalVotingModule); } + /// @notice Helper function to set CouncilMemberElections proposal type data. + function _setCouncilMemberElectionsProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.CouncilMemberElections, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: APPROVAL_VOTING_MODULE_ID + }) + ); + } + /// @notice Helper function to set GovernanceFund proposal type data. function _setGovernanceFundProposalType() internal { _setProposalTypeData( ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }) ); } @@ -159,7 +179,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }) ); } @@ -169,7 +189,6 @@ contract ProposalValidator_Init is CommonTest { _setGovernanceFundProposalType(); _setCouncilBudgetProposalType(); } - /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() internal @@ -213,11 +232,11 @@ contract ProposalValidator_Init is CommonTest { }); proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: FUNDING_PROPOSALS_VOTING_MODULE + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); return (proposalTypes, proposalTypesData); @@ -271,11 +290,49 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(options, settings); } + /// @notice Helper function to construct voting module data for council elections + function _constructCouncilElectionVotingModuleData( + string[] memory descriptions, + uint128 criteriaValue + ) + internal + pure + returns (bytes memory) + { + // Construct ProposalOption array for elections (no execution calls) + ProposalOption[] memory options = new ProposalOption[](descriptions.length); + + for (uint256 i = 0; i < descriptions.length; i++) { + address[] memory targets = new address[](0); + uint256[] memory values = new uint256[](0); + bytes[] memory calldatas = new bytes[](0); + + options[i] = ProposalOption({ + budgetTokensSpent: 0, + targets: targets, + values: values, + calldatas: calldatas, + description: descriptions[i] + }); + } + + // Construct ProposalSettings with TopChoices criteria + ProposalSettings memory settings = ProposalSettings({ + maxApprovals: uint8(descriptions.length), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), + criteriaValue: criteriaValue, + budgetAmount: 0 + }); + + return abi.encode(options, settings); + } + /// @notice Helper function to setup proposal types configurator mocks function _setupProposalTypesConfiguratorMocks() internal { // Mock calls for different proposal type IDs for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 2 || i == FUNDING_PROPOSALS_VOTING_MODULE) ? approvalVotingModule : address(0); + address moduleAddress = (i == 2 || i == APPROVAL_VOTING_MODULE_ID) ? approvalVotingModule : address(0); vm.mockCall( address(proposalTypesConfigurator), @@ -846,7 +903,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, "Test proposal", proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType ); } @@ -1133,12 +1190,12 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - // Both GovernanceFund and CouncilBudget use FUNDING_PROPOSALS_VOTING_MODULE + // Both GovernanceFund and CouncilBudget use APPROVAL_VOTING_MODULE_ID if ( proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget ) { - assertEq(proposalVotingModule, FUNDING_PROPOSALS_VOTING_MODULE); + assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); } else { assertEq(proposalVotingModule, uint8(i)); } @@ -1184,3 +1241,227 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); } } + +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test +/// @notice Happy path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + string proposalDescription; + + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + + function setUp() public override { + super.setUp(); + + _setCouncilMemberElectionsProposalType(); + + proposalDescription = "Council Member Elections Q4 2024"; + } + + function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { + optionCount = uint8(bound(optionCount, 2, type(uint8).max)); // Minimum 2 options to have valid criteria < + // optionCount + criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount + + // Create dynamic array of option descriptions based on option count + string[] memory optionDescriptions = new string[](optionCount); + for (uint256 i = 0; i < optionCount; i++) { + optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); + } + + // Create attestation for the proposal + bytes32 attestationUid = + _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Expect ProposalSubmitted event + vm.expectEmit(address(validator)); + emit ProposalSubmitted( + expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections + ); + + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(topDelegate_A); + bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + (address proposer, ProposalValidator.ProposalType proposalType, bool inVoting, uint256 approvalCount) = + validator.getProposalData(proposalHash); + + assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); + assertEq( + uint8(proposalType), + uint8(ProposalValidator.ProposalType.CouncilMemberElections), + "Proposal type should be CouncilMemberElections" + ); + assertFalse(inVoting, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail +/// @notice Sad path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionDescriptions; + string proposalDescription; + bytes32 attestationUid; + + function setUp() public override { + super.setUp(); + + _setCouncilMemberElectionsProposalType(); + + criteriaValue = 2; + optionDescriptions = new string[](3); + optionDescriptions[0] = "Candidate A"; + optionDescriptions[1] = "Candidate B"; + optionDescriptions[2] = "Candidate C"; + + proposalDescription = "Test Council Elections"; + attestationUid = _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + } + + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) + public + { + vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } + + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { + string[] memory emptyOptions = new string[](0); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal(criteriaValue, emptyOptions, proposalDescription, attestationUid); + } + + function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + + // Create new attestation for second attempt + bytes32 secondAttestation = + _createAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(topDelegate_B); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, secondAttestation + ); + } + + function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation + ); + } + + function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( + uint128 invalidCriteriaValue + ) + public + { + // Bound invalidCriteriaValue to be greater than options length + invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid + ); + } +} From 3db606428f3f053171e69a3282db17c143cc08d9 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:46:14 +0300 Subject: [PATCH 47/73] feat: approve proposal (#427) * fix: change attestation storage var name * feat: add approve proposal impl and tests * fix: pre-pr * fix: conflicts * chore: improve natspec * refactor: improve can approve --- .../governance/IProposalValidator.sol | 69 ++- .../snapshots/abi/ProposalValidator.json | 94 ++- .../storageLayout/ProposalValidator.json | 15 +- .../src/governance/ProposalValidator.sol | 136 +++-- .../test/governance/ProposalValidator.t.sol | 542 ++++++++++++------ 5 files changed, 519 insertions(+), 337 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index aa787840981..beca086f0c1 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -13,15 +13,16 @@ interface IProposalValidator is ISemver { error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); - error ProposalValidator_InsufficientVotingPower(); error ProposalValidator_InvalidAttestation(); - error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_VotingCycleAlreadySet(); + error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_ProposalTypesDataLengthMismatch(); error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); + error ProposalValidator_AttestationRevoked(); + error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); struct ProposalData { @@ -36,7 +37,7 @@ interface IProposalValidator is ISemver { uint256 requiredApprovals; uint8 proposalVotingModule; } - + enum ProposalType { ProtocolOrGovernorUpgrade, MaintenanceUpgrade, @@ -45,6 +46,13 @@ interface IProposalValidator is ISemver { CouncilBudget } + event ProposalSubmitted( + bytes32 indexed proposalHash, + address indexed proposer, + string description, + ProposalType proposalType + ); + event ProposalApproved( bytes32 indexed proposalHash, address indexed approver @@ -55,9 +63,12 @@ interface IProposalValidator is ISemver { address indexed executor ); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - event MinimumVotingPowerSet(uint256 newMinimumVotingPower); + event VotingCycleDataSet( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 votingCycleDistributionLimit + ); event DistributionThresholdSet(uint256 newDistributionThreshold); @@ -71,24 +82,12 @@ interface IProposalValidator is ISemver { bytes32 indexed proposalHash, bytes encodedVotingModuleData ); - - event VotingCycleDataSet( - uint256 cycleNumber, - uint256 startBlock, - uint256 duration, - uint256 votingCycleDistributionLimit - ); - event ProposalSubmitted( - bytes32 indexed proposalHash, - address indexed proposer, - string description, - ProposalType proposalType - ); - + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event Initialized(uint8 version); - function approveProposal(bytes32 _proposalHash) external; + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; function moveToVote( address[] memory _targets, @@ -96,8 +95,6 @@ interface IProposalValidator is ISemver { bytes[] memory _calldatas, string memory _description ) external returns (uint256 governorProposalId_); - - function setMinimumVotingPower(uint256 _minimumVotingPower) external; function setDistributionThreshold(uint256 _distributionThreshold) external; @@ -105,7 +102,7 @@ interface IProposalValidator is ISemver { ProposalType _proposalType, ProposalTypeData memory _proposalTypeData ) external; - + function setVotingCycleData( uint256 _cycleNumber, uint256 _startBlock, @@ -132,7 +129,6 @@ interface IProposalValidator is ISemver { function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, - uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, uint256 _duration, @@ -141,14 +137,12 @@ interface IProposalValidator is ISemver { ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; - + function renounceOwnership() external; - - function canSignOff(address _delegate) external view returns (bool canSignOff_); - - function transferOwnership(address newOwner) external; - function minimumVotingPower() external view returns (uint256); + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + + function transferOwnership(address newOwner) external; function distributionThreshold() external view returns (uint256); @@ -160,20 +154,23 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + + function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); - + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( - uint256 startingBlock, - uint256 duration, + uint256 startingBlock, + uint256 duration, uint256 votingCycleDistributionLimit ); function __constructor__( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _votingToken ) external; diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index ec85035250f..958fb0af472 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -3,7 +3,12 @@ "inputs": [ { "internalType": "bytes32", - "name": "_attestationSchemaUid", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", "type": "bytes32" }, { @@ -22,7 +27,7 @@ }, { "inputs": [], - "name": "ATTESTATION_SCHEMA_UID", + "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", "outputs": [ { "internalType": "bytes32", @@ -46,6 +51,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "VOTING_TOKEN", @@ -65,6 +83,11 @@ "internalType": "bytes32", "name": "_proposalHash", "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" } ], "name": "approveProposal", @@ -74,17 +97,22 @@ }, { "inputs": [ + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + }, { "internalType": "address", "name": "_delegate", "type": "address" } ], - "name": "canSignOff", + "name": "canApproveProposal", "outputs": [ { "internalType": "bool", - "name": "canSignOff_", + "name": "canApprove_", "type": "bool" } ], @@ -129,11 +157,6 @@ "name": "_proposalTypesConfigurator", "type": "address" }, - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - }, { "internalType": "uint256", "name": "_cycleNumber", @@ -187,19 +210,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "minimumVotingPower", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -304,19 +314,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_minimumVotingPower", - "type": "uint256" - } - ], - "name": "setMinimumVotingPower", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -534,19 +531,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newMinimumVotingPower", - "type": "uint256" - } - ], - "name": "MinimumVotingPowerSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -710,6 +694,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationRevoked", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ExceedsDistributionThreshold", @@ -722,12 +711,17 @@ }, { "inputs": [], - "name": "ProposalValidator_InsufficientVotingPower", + "name": "ProposalValidator_InvalidAttestation", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestation", + "name": "ProposalValidator_InvalidAttestationSchema", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidCriteriaValue", "type": "error" }, { diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 8e422ff0af2..50c94894f8b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -41,39 +41,32 @@ "slot": "101", "type": "contract IProposalTypesConfigurator" }, - { - "bytes": "32", - "label": "minimumVotingPower", - "offset": 0, - "slot": "102", - "type": "uint256" - }, { "bytes": "32", "label": "distributionThreshold", "offset": 0, - "slot": "103", + "slot": "102", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "105", + "slot": "104", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "105", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 91769149ae3..ed1fccc64a0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -37,9 +37,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when attempting to move a proposal to vote that is already in voting. error ProposalValidator_ProposalAlreadySubmitted(); - /// @notice Thrown when a delegate has insufficient voting power to approve a proposal. - error ProposalValidator_InsufficientVotingPower(); - /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); @@ -61,6 +58,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the options length is invalid (zero or exceeds uint8 max). error ProposalValidator_InvalidOptionsLength(); + /// @notice Thrown when an attestation is revoked. + error ProposalValidator_AttestationRevoked(); + + /// @notice Thrown when an attestation schema is invalid. + error ProposalValidator_InvalidAttestationSchema(); + /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). error ProposalValidator_InvalidCriteriaValue(); @@ -142,10 +145,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param executor The address that executed the move to vote. event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); - /// @notice Emitted when the minimum voting power is set. - /// @param newMinimumVotingPower The new minimum voting power. - event MinimumVotingPowerSet(uint256 newMinimumVotingPower); - /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. /// @param startBlock The block number of the starting block of the voting cycle. @@ -170,22 +169,24 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param encodedVotingModuleData The encoded voting module data. event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - /// @notice The schema UID for attestations in the Ethereum Attestation Service. + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller + /// is an approved proposer. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } - bytes32 public immutable ATTESTATION_SCHEMA_UID; + bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller + /// is part of the top100 delegates. + bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The token used to determine voting power. + /// @notice The governance token contract. IGovernanceToken public immutable VOTING_TOKEN; /// @notice The proposal types configurator contract. IProposalTypesConfigurator public proposalTypesConfigurator; - /// @notice The minimum voting power required for a delegate to approve proposals. - uint256 public minimumVotingPower; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public distributionThreshold; @@ -205,17 +206,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Constructs the ProposalValidator contract. - /// @param _attestationSchemaUid The schema UID for attestations in EAS. + /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. + /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller + /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. /// @param _votingToken The token used to determine voting power. constructor( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _votingToken ) ReinitializableBase(1) { - ATTESTATION_SCHEMA_UID = _attestationSchemaUid; + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; GOVERNOR = _governor; VOTING_TOKEN = _votingToken; _disableInitializers(); @@ -224,7 +229,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. /// @param _proposalTypesConfigurator The proposal types configurator contract address. - /// @param _minimumVotingPower The minimum voting power required for a delegate to approve proposals. /// @param _cycleNumber The number of the current voting cycle. /// @param _startBlock The block number of the starting block of the voting cycle. /// @param _duration The duration of the voting cycle. @@ -235,7 +239,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, - uint256 _minimumVotingPower, uint256 _cycleNumber, uint256 _startBlock, uint256 _duration, @@ -252,7 +255,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposalTypesConfigurator = _proposalTypesConfigurator; - _setMinimumVotingPower(_minimumVotingPower); _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); @@ -281,7 +283,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Validate EAS attestation - must be called by owner-approved address - _validateAttestation(_attestationUid, ProposalType.CouncilMemberElections); + _validateApprovedProposerAttestation(_attestationUid, ProposalType.CouncilMemberElections); // Validate options length bounds uint256 optionsLength = _optionDescriptions.length; @@ -456,23 +458,31 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } - /// @notice Approve a proposal (only callable by delegates with sufficient voting power) + /// @notice Approves a proposal before being moved for voting. + /// @dev This function should only be called by the top delegates. /// @param _proposalHash The hash of the proposal to approve - function approveProposal(bytes32 _proposalHash) external { - if (!canSignOff(msg.sender)) { - revert ProposalValidator_InsufficientVotingPower(); - } - + /// @param _attestationUid The UID of the attestation for the delegate to approve the proposal + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external { + address _delegate = _msgSender(); ProposalData storage proposal = _proposals[_proposalHash]; + // check if the proposal exists + if (proposal.proposer == address(0)) { + revert ProposalValidator_ProposalDoesNotExist(); + } - if (proposal.delegateApprovals[msg.sender]) { + // check if the caller has already approved the proposal + if (proposal.delegateApprovals[_delegate]) { revert ProposalValidator_ProposalAlreadyApproved(); } - proposal.delegateApprovals[msg.sender] = true; + // validate the attestation + _validateTopDelegateAttestation(_attestationUid, _msgSender()); + + // store the approval + proposal.delegateApprovals[_delegate] = true; proposal.approvalCount++; - emit ProposalApproved(_proposalHash, msg.sender); + emit ProposalApproved(_proposalHash, _delegate); } /// @notice Move a proposal to voting phase after sufficient delegate approvals @@ -516,17 +526,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalMovedToVote(_proposalHash, msg.sender); } - /// @notice Returns whether a delegate has enough voting power to approve a proposal. - /// @param _delegate The address of the delegate to check. - /// @return canSignOff_ True if the delegate has sufficient voting power, false otherwise. - function canSignOff(address _delegate) public view returns (bool canSignOff_) { - canSignOff_ = VOTING_TOKEN.balanceOf(_delegate) >= minimumVotingPower; - } - - /// @notice Sets the minimum voting power required for a delegate to approve proposals. - /// @param _minimumVotingPower The new minimum voting power threshold. - function setMinimumVotingPower(uint256 _minimumVotingPower) external onlyOwner { - _setMinimumVotingPower(_minimumVotingPower); + /// @notice Checks if a delegate can approve a proposal. + /// @dev Helper function for UI integration. + /// @param _attestationUid The UID of the attestation to check. + /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); } /// @notice Sets the data of a voting cycle. @@ -571,7 +576,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// Reverts with ProposalValidator_InvalidAttestation if validation fails. /// @param _attestationUid The UID of the attestation to validate. /// @param _expectedProposalType The expected proposal type from the attestation. - function _validateAttestation(bytes32 _attestationUid, ProposalType _expectedProposalType) internal view { + function _validateApprovedProposerAttestation( + bytes32 _attestationUid, + ProposalType _expectedProposalType + ) + internal + view + { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) @@ -582,13 +593,47 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( - attestation.attester != owner() || attestation.schema != ATTESTATION_SCHEMA_UID + attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } } + /// @notice Validates the attestation data for a delegate that tries to approve a proposal. + /// @dev Only acceptes attestations that does NOT include partial delegation. + /// @param _attestationUid The UID of the attestation to validate. + /// @param _delegate The delegate to validate the attestation for. + /// @return canApprove_ True if the attestation is valid, false otherwise. + function _validateTopDelegateAttestation( + bytes32 _attestationUid, + address _delegate + ) + internal + view + returns (bool canApprove_) + { + Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + + // check if the schema is correct + if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { + revert ProposalValidator_InvalidAttestationSchema(); + } + + // check if the attestation is revoked + if (attestation.revocationTime != 0) { + revert ProposalValidator_AttestationRevoked(); + } + + // check if the attestation includes partial delegation or the recipient is not the caller + if (_includePartialDelegation || attestation.recipient != _delegate) { + revert ProposalValidator_InvalidAttestation(); + } + + canApprove_ = true; + } + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. @@ -606,13 +651,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); } - /// @notice Private function to set the minimum voting power and emit event. - /// @param _minimumVotingPower The new minimum voting power threshold. - function _setMinimumVotingPower(uint256 _minimumVotingPower) private { - minimumVotingPower = _minimumVotingPower; - emit MinimumVotingPowerSet(_minimumVotingPower); - } - /// @notice Private function to set the voting cycle data and emit event. /// @param _cycleNumber The number of the voting cycle to set. /// @param _startBlock The block number of the starting block of the voting cycle. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 12593e24e7b..bd4cc5aa594 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -6,7 +6,13 @@ import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; -import { IEAS, AttestationRequest, AttestationRequestData } from "src/vendor/eas/IEAS.sol"; +import { + IEAS, + AttestationRequest, + AttestationRequestData, + RevocationRequest, + RevocationRequestData +} from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -32,11 +38,17 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @notice A test contract that exposes the private _hashProposal function contract ProposalValidatorForTest is ProposalValidator { constructor( - bytes32 _attestationSchemaUid, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor, IGovernanceToken _governanceToken ) - ProposalValidator(_attestationSchemaUid, _governor, _governanceToken) + ProposalValidator( + _approvedProposerAttestationSchemaUid, + _topDelegatesAttestationSchemaUid, + _governor, + _governanceToken + ) { } function hashProposalWithModule( @@ -61,6 +73,25 @@ contract ProposalValidatorForTest is ProposalValidator { return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); } + function setProposalData( + bytes32 _proposalHash, + address _proposer, + ProposalType _proposalType, + bool _inVoting, + uint256 _approvalCount + ) + public + { + _proposals[_proposalHash].proposer = _proposer; + _proposals[_proposalHash].proposalType = _proposalType; + _proposals[_proposalHash].inVoting = _inVoting; + _proposals[_proposalHash].approvalCount = _approvalCount; + } + + function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { + _proposals[_proposalHash].delegateApprovals[_delegate] = true; + } + /// @notice Check if a delegate has approved a proposal function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { return _proposals[_proposalHash].delegateApprovals[_delegate]; @@ -72,29 +103,32 @@ contract ProposalValidatorForTest is ProposalValidator { contract ProposalValidator_Init is CommonTest { using stdStorage for StdStorage; - uint256 public constant TOP_DELEGATE_VOTING_POWER = 10000 ether; // 10k OP uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; uint256 public constant DURATION = 100; uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; - uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; address owner; address user; - address topDelegate_A; - address topDelegate_B; - address topDelegate_C; - address topDelegate_D; + address topDelegate_A = makeAddr("topDelegate_A"); + address topDelegate_B = makeAddr("topDelegate_B"); + address topDelegate_C = makeAddr("topDelegate_C"); + address topDelegate_D = makeAddr("topDelegate_D"); + bytes32 topDelegateAttestation_A; + bytes32 topDelegateAttestation_B; + bytes32 topDelegateAttestation_C; + bytes32 topDelegateAttestation_D; address approvalVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; IOptimismGovernor public governor; IProposalTypesConfigurator public proposalTypesConfigurator; - bytes32 public ATTESTATION_SCHEMA_UID; + bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; bytes32 public proposalHash; event ProposalSubmitted( @@ -120,21 +154,6 @@ contract ProposalValidator_Init is CommonTest { vm.expectCall(_receiver, _calldata); } - /// @notice Helper function to make a top delegate. - function _makeTopDelegate(string memory _name) internal returns (address) { - address delegate = makeAddr(_name); - deal(address(governanceToken), delegate, TOP_DELEGATE_VOTING_POWER); - vm.prank(delegate); - governanceToken.delegate(delegate); - return delegate; - } - - /// @notice Helper function to make a (top) delegate approve a proposal. - function _approveProposal(address _delegate, bytes32 _proposalHash) internal { - vm.prank(_delegate); - validator.approveProposal(_proposalHash); - } - /// @notice Helper function to set proposal type data using StdStorage. function _setProposalTypeData( ProposalValidator.ProposalType _proposalType, @@ -190,6 +209,7 @@ contract ProposalValidator_Init is CommonTest { _setCouncilBudgetProposalType(); } /// @notice Helper to create minimal valid arrays for funding proposal error tests + function _createMinimalFundingArrays() internal pure @@ -363,7 +383,9 @@ contract ProposalValidator_Init is CommonTest { // Setup mocks _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -374,7 +396,6 @@ contract ProposalValidator_Init is CommonTest { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -395,21 +416,28 @@ contract ProposalValidator_Init is CommonTest { governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); + // Create schemas vm.prank(owner); - ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false ); + vm.prank(owner); + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100,bool includePartialDelegation,string date", ISchemaResolver(address(0)), true + ); + _initializeValidator(); - topDelegate_A = _makeTopDelegate("topDelegate_A"); - topDelegate_B = _makeTopDelegate("topDelegate_B"); - topDelegate_C = _makeTopDelegate("topDelegate_C"); - topDelegate_D = _makeTopDelegate("topDelegate_D"); + // Create attestations for top delegates + topDelegateAttestation_A = _createTopDelegateAttestation(topDelegate_A); + topDelegateAttestation_B = _createTopDelegateAttestation(topDelegate_B); + topDelegateAttestation_C = _createTopDelegateAttestation(topDelegate_C); + topDelegateAttestation_D = _createTopDelegateAttestation(topDelegate_D); } - /// @notice Helper to create a valid attestation for a proposal - function _createAttestation( + /// @notice Helper to create a valid attestation for an approved proposer + function _createApprovedProposerAttestation( address _delegate, ProposalValidator.ProposalType _proposalType ) @@ -419,7 +447,7 @@ contract ProposalValidator_Init is CommonTest { vm.prank(owner); return IEAS(Predeploys.EAS).attest( AttestationRequest({ - schema: ATTESTATION_SCHEMA_UID, + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ recipient: address(0), expirationTime: 0, @@ -432,6 +460,24 @@ contract ProposalValidator_Init is CommonTest { ); } + /// @notice Helper to create a valid attestation for a top delegate + function _createTopDelegateAttestation(address _delegate) internal returns (bytes32) { + vm.prank(owner); + return IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: _delegate, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); + } + /// @notice Helper to create a standard proposal setup function _createProposalSetup() internal @@ -456,40 +502,30 @@ contract ProposalValidator_Init is CommonTest { /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - function setUp() public override { - super.setUp(); + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Ensure the proposal hash is not 0 + vm.assume(_proposalHash != bytes32(0)); - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - } + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - function test_approveProposal_succeeds() public { // Expect event to be emitted when approving vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_A); - _approveProposal(topDelegate_A, proposalHash); + emit ProposalApproved(_proposalHash, topDelegate_A); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_B); - _approveProposal(topDelegate_B, proposalHash); + // Approve the proposal, use the attestation of the top delegate that was created in setUp + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_C); - _approveProposal(topDelegate_C, proposalHash); + // Check that the proposal data has been updated + assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(proposalHash, topDelegate_D); - _approveProposal(topDelegate_D, proposalHash); + (,,, uint256 approvalCount) = validator.getProposalData(_proposalHash); + assertEq(approvalCount, 1); } } @@ -498,165 +534,287 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { function setUp() public override { super.setUp(); - - (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) = - _createProposalSetup(); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - uint8 proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); - - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented } - function test_approveProposal_insufficientVotingPower_reverts() public { - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientVotingPower.selector); - _approveProposal(user, proposalHash); + function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + // There is no stored proposal data so this will revert + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function test_approveProposal_alreadyApproved_reverts() public { - _approveProposal(topDelegate_A, proposalHash); + function test_approveProposal_proposalAlreadyApproved_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // Mock the proposal as already approved by the top delegate + validator.mockApproveProposal(_proposalHash, topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - _approveProposal(topDelegate_A, proposalHash); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} - -/// @title ProposalValidator_MoveToVote_Test -/// @notice Happy path tests for moveToVote function -contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; - ProposalValidator.ProposalType proposalType; - uint8 proposalVotingModule; - function setUp() public override { - super.setUp(); + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - (targets, values, calldatas, description) = _createProposalSetup(); + // create a new schema + vm.prank(topDelegate_A); + bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100, string date", ISchemaResolver(address(0)), true + ); - proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + // create an attestation with the new schema + vm.prank(topDelegate_A); + bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: _invalidSchemaUid, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); - _approveProposal(topDelegate_D, proposalHash); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _invalidAttestationUid); } - function test_moveToVote_succeeds() public { - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); - - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(proposalHash, owner); + function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // revoke the attestation vm.prank(owner); - uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) + }) + ); - assertEq(governorProposalId, 1); + vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} -/// @title ProposalValidator_MoveToVote_TestFail -/// @notice Sad path tests for moveToVote function -contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; - ProposalValidator.ProposalType proposalType; - uint8 proposalVotingModule; - - function setUp() public override { - super.setUp(); - - (targets, values, calldatas, description) = _createProposalSetup(); - - proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalVotingModule = 0; - bytes32 attestationUid = _createAttestation(topDelegate_A, proposalType); + function test_approveProposal_invalidAttestationCaller_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + address _caller + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - /* vm.prank(topDelegate_A); */ - proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - } + // Ensure the caller is not a top delegate + vm.assume( + _caller != topDelegate_A && _caller != topDelegate_B && _caller != topDelegate_C && _caller != topDelegate_D + ); - function test_moveToVote_insufficientApprovals_reverts() public { - // Only approve with 3 delegates (need 4) - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(_caller); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function test_moveToVote_alreadyProposed_reverts() public { - // Approve with all 4 delegates - _approveProposal(topDelegate_A, proposalHash); - _approveProposal(topDelegate_B, proposalHash); - _approveProposal(topDelegate_C, proposalHash); - _approveProposal(topDelegate_D, proposalHash); + function test_approveProposal_invalidAttestationPartialDelegation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, proposalVotingModule)), - abi.encode(1) - ); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + // create an attestation with partial delegation vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", true, "2000-01-01"), + value: 0 + }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.prank(owner); - validator.moveToVote(targets, values, calldatas, description); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); } } -/// @title ProposalValidator_Getters_Test -/// @notice Tests for getter functions -contract ProposalValidator_Getters_Test is ProposalValidator_Init { - function test_canSignOff_succeeds() public { - bool canSignOff = validator.canSignOff(topDelegate_A); - assertTrue(canSignOff); +// /// @title ProposalValidator_MoveToVote_Test +// /// @notice Happy path tests for moveToVote function +// contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { +// address[] targets; +// uint256[] values; +// bytes[] calldatas; +// string description; +// ProposalValidator.ProposalType proposalType; +// uint8 proposalVotingModule; + +// function setUp() public override { +// super.setUp(); + +// (targets, values, calldatas, description) = _createProposalSetup(); + +// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; +// proposalVotingModule = 0; +// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + +// /* vm.prank(topDelegate_A); */ +// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented + +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); +// _approveProposal(topDelegate_D, proposalHash); +// } + +// function test_moveToVote_succeeds() public { +// _mockAndExpect( +// address(governor), +// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, +// proposalVotingModule)), +// abi.encode(1) +// ); + +// // Expect the ProposalMovedToVote event to be emitted +// vm.expectEmit(address(validator)); +// emit ProposalMovedToVote(proposalHash, owner); + +// vm.prank(owner); +// uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); + +// assertEq(governorProposalId, 1); +// } +// } + +// /// @title ProposalValidator_MoveToVote_TestFail +// /// @notice Sad path tests for moveToVote function +// contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { +// address[] targets; +// uint256[] values; +// bytes[] calldatas; +// string description; +// ProposalValidator.ProposalType proposalType; +// uint8 proposalVotingModule; + +// function setUp() public override { +// super.setUp(); + +// (targets, values, calldatas, description) = _createProposalSetup(); + +// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; +// proposalVotingModule = 0; +// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + +// /* vm.prank(topDelegate_A); */ +// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented +// } + +// function test_moveToVote_insufficientApprovals_reverts() public { +// // Only approve with 3 delegates (need 4) +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); + +// vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); +// } + +// function test_moveToVote_alreadyProposed_reverts() public { +// // Approve with all 4 delegates +// _approveProposal(topDelegate_A, proposalHash); +// _approveProposal(topDelegate_B, proposalHash); +// _approveProposal(topDelegate_C, proposalHash); +// _approveProposal(topDelegate_D, proposalHash); + +// _mockAndExpect( +// address(governor), +// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, +// proposalVotingModule)), +// abi.encode(1) +// ); + +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); + +// vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); +// vm.prank(owner); +// validator.moveToVote(targets, values, calldatas, description); +// } +// } + +/// @title ProposalValidator_CanApproveProposal_Test +/// @notice Tests for the canApproveProposal function +contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { + function test_canApproveProposal_ReturnsTrue_succeeds() public { + // Attestation already created in setUp + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + assertTrue(canApprove); + } + + function test_canApproveProposal_ReturnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + // Ensure the attestation uid is not one of the top delegates + vm.assume( + _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B + && _attestationUid != topDelegateAttestation_C && _attestationUid != topDelegateAttestation_D + ); - bool cannotSignOff = validator.canSignOff(user); - assertFalse(cannotSignOff); + bool canApprove; + // Expect the invalid attestation error to be reverted + vm.expectRevert(); + try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result) { + canApprove = result; + } catch { + canApprove = false; + } + + assertEq(canApprove, false); } } /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { - function testFuzz_setMinimumVotingPower_succeeds(uint256 newMinimumVotingPower) public { - // Expect the MinimumVotingPowerSet event to be emitted - vm.expectEmit(address(validator)); - emit MinimumVotingPowerSet(newMinimumVotingPower); - - vm.prank(owner); - validator.setMinimumVotingPower(newMinimumVotingPower); - - assertEq(validator.minimumVotingPower(), newMinimumVotingPower); - } - - function test_setMinimumVotingPower_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setMinimumVotingPower(10000 ether); - } - function testFuzz_setVotingCycleData_succeeds( uint256 cycleNumber, uint256 startBlock, @@ -1143,7 +1301,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Setup mocks _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest(ATTESTATION_SCHEMA_UID, governor, governanceToken); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); // Initialize will be tested manually } @@ -1162,7 +1322,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -1175,7 +1334,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); // Verify initialization was successful - assertEq(validator.minimumVotingPower(), MINIMUM_VOTING_POWER); assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); @@ -1228,7 +1386,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ( owner, proposalTypesConfigurator, - MINIMUM_VOTING_POWER, CYCLE_NUMBER, START_BLOCK, DURATION, @@ -1270,7 +1427,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal // Create attestation for the proposal bytes32 attestationUid = - _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); // Calculate expected proposal hash bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); @@ -1337,7 +1494,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop optionDescriptions[2] = "Candidate C"; proposalDescription = "Test Council Elections"; - attestationUid = _createAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) @@ -1393,7 +1551,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Create new attestation for second attempt bytes32 secondAttestation = - _createAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + _createApprovedProposerAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); // Attempt to submit identical proposal should revert vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); @@ -1424,14 +1582,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } - function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) + public + { vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner // Create attestation but don't use proper owner as attester vm.prank(fuzzedAttester); // Not the owner bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( AttestationRequest({ - schema: ATTESTATION_SCHEMA_UID, + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ recipient: address(0), expirationTime: 0, From 8dd18caf8e9926901669663222a95ea1515a2c08 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:13:40 -0300 Subject: [PATCH 48/73] feat: add submit upgrade proposal (#429) * feat: add submitFundingProposal function * fix: compiler errors * test: add submitFundingProposal tests * chore: run pre-pr * chore: add comments for submitFundingProposal * docs: improve natspec * feat: add validation for options length in ProposalValidator * feat: get votingModuleAddress from ProposalTypeConfigurator * feat: check for proposal existance on submission * test: add InvalidOptionsLength tests * chore: run pre-pr * feat: add submitCouncilMemberElectionsProposal function * chore: run pre-pr * fix: remove duplicated tests * test(fuzz): use fuzz testing for happy paths * feat: add check for criteria value < optionslength * perf: optimiza for loops usage * test: fuzz invalid attestationUid * test: fuzz invalid proposer * fix: lack of attestation existance validation * test: fuzz exceeded max options test * test: fuzz unapproved attester * test: reduce upper bound for array size * test: remove duplicated test * fix: update criteria value validation logic in ProposalValidator * refactor(test): use helper functions for duplicated logic * refactor: declare approvalVotingModule variable * test: increment the assertions in council memeber election tests * refactor(test): improve tests legibility * test: use fuzzing instead of individual tests for edge cases * test: improve submitFundingProposal assertions * test: fuzz proposer * chore(test): remove unused setup variables * test: use fuzzing for sad submitFundingProposal paths * chore: remove redundant tests * test: use fuzzing for exceeded amount * chore: run pre-pr * refactor: normalize funding proposal voting modules in global variable * test: fuzz proposal types for revert cases * refactor: use minimal values for funding proposal revert cases tests * refactor: use more descriptive variables for testing * chore: run pre-pr * refactor: use variables instead of hardcoded values * chore: rename voting module depending on configurator instead of internal proposal type * chore: improve tests naming * test: expect proposal type configurator calls * feat: add submitUpgradeProposal function * refactor: improve variable names consistency * test: add submitUpgradeProposal tests * chore: run pre-pr * refactpr(test): separate submitUpgradePropowal tests depending on proposal type * test: improve coverage on InvalidProposalType tests * chore: improve code legibility * chore: improve comments Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> * fix: merge conflicts * fix: broken compile for missing variable * fix: broken tests out of enum bounds * fix: pre-pr * fix: correct order for event emission * refactor(test): declare events in Init contract * refactor: use constants for code legibility --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .semgrep/rules/sol-rules.yaml | 3 + .../governance/IOptimismGovernor.sol | 12 + .../governance/IProposalValidator.sol | 17 +- .../snapshots/abi/OptimisticModule.json | 258 ++++++++++ .../snapshots/abi/ProposalValidator.json | 45 +- .../snapshots/semver-lock.json | 4 +- .../storageLayout/OptimisticModule.json | 9 + .../src/governance/OptimisticModule.sol | 154 ++++++ .../src/governance/ProposalValidator.sol | 104 +++- .../test/governance/ProposalValidator.t.sol | 457 ++++++++++++++++-- 10 files changed, 1015 insertions(+), 48 deletions(-) create mode 100644 packages/contracts-bedrock/snapshots/abi/OptimisticModule.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json create mode 100644 packages/contracts-bedrock/src/governance/OptimisticModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index b5c7e67371d..ec576a400c9 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -133,6 +133,7 @@ rules: - packages/contracts-bedrock/src/governance/GovernanceToken.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -159,6 +160,7 @@ rules: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -264,6 +266,7 @@ rules: - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol + - packages/contracts-bedrock/src/governance/OptimisticModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index 39dd12b664a..994bb597df4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {VotingModule} from "src/governance/VotingModule.sol"; +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; interface IOptimismGovernor { function propose( @@ -21,6 +22,17 @@ interface IOptimismGovernor { function timelock() external view returns (address); + function PROPOSAL_TYPES_CONFIGURATOR() external view returns (address); + + function token() external view returns (IVotesUpgradeable); + + function getProposalType(uint256 proposalId) external view returns (uint8); + + function proposalVotes(uint256 proposalId) + external + view + returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); + /// @notice Returns the snapshot block number for a proposal, 0 if proposal doesn't exist /// @param proposalId The ID of the proposal /// @return The snapshot block number, or 0 if proposal doesn't exist diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index beca086f0c1..815b321a5df 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -24,6 +24,8 @@ interface IProposalValidator is ISemver { error ProposalValidator_AttestationRevoked(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); + error ProposalValidator_InvalidUpgradeProposalType(); + error ProposalValidator_InvalidAgainstThreshold(); struct ProposalData { address proposer; @@ -83,10 +85,10 @@ interface IProposalValidator is ISemver { bytes encodedVotingModuleData ); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event Initialized(uint8 version); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; function moveToVote( @@ -126,6 +128,13 @@ interface IProposalValidator is ISemver { ProposalType _proposalType ) external returns (bytes32 proposalHash_); + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType + ) external returns (bytes32 proposalHash_); + function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, @@ -140,10 +149,10 @@ interface IProposalValidator is ISemver { function renounceOwnership() external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); - function transferOwnership(address newOwner) external; + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + function distributionThreshold() external view returns (uint256); function VOTING_TOKEN() external view returns (IGovernanceToken); diff --git a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json new file mode 100644 index 00000000000..f2e29a066bd --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json @@ -0,0 +1,258 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_governor", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "COUNTING_MODE", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "PERCENT_DIVISOR", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PROPOSAL_DATA_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "VOTE_PARAMS_ENCODING", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "_countVote", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "_formatExecuteParams", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "", + "type": "bytes[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + } + ], + "name": "_voteSucceeded", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "proposals", + "outputs": [ + { + "internalType": "address", + "name": "governor", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint248", + "name": "againstThreshold", + "type": "uint248" + }, + { + "internalType": "bool", + "name": "isRelativeToVotableSupply", + "type": "bool" + } + ], + "internalType": "struct ProposalSettings", + "name": "settings", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_proposalData", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "_descriptionHash", + "type": "bytes32" + } + ], + "name": "propose", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ExistingProposal", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParams", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernor", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_NotOptimisticProposalType", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_OptimisticModuleOnlySignal", + "type": "error" + }, + { + "inputs": [], + "name": "OptimisticModule_WrongProposalId", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 958fb0af472..9b982f52282 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -450,6 +450,40 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint248", + "name": "_againstThreshold", + "type": "uint248" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_attestationUid", + "type": "bytes32" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "submitUpgradeProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -711,17 +745,17 @@ }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestation", + "name": "ProposalValidator_InvalidAgainstThreshold", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidAttestationSchema", + "name": "ProposalValidator_InvalidAttestation", "type": "error" }, { "inputs": [], - "name": "ProposalValidator_InvalidCriteriaValue", + "name": "ProposalValidator_InvalidAttestationSchema", "type": "error" }, { @@ -739,6 +773,11 @@ "name": "ProposalValidator_InvalidOptionsLength", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidUpgradeProposalType", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index a04d1d919ce..4333151e9c8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xe5a13d76f05b05db718ac645ee1aaf8770de307b8f4e367e3def458d1be6ba3f", - "sourceCodeHash": "0x2e2655c25888e6d3502374d7e257888ecc300188efbc18c44b19fc6640b00629" + "initCodeHash": "0x5c805e6dba872f7d2f16cb27a1ab7f8b31813e2b79b04d5ea61055bd06f0bf74", + "sourceCodeHash": "0xc1b29b287e1ff7df81aa3bc740fe63fc0203a976d222b1241636b639a478b791" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json new file mode 100644 index 00000000000..a600d98d300 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "32", + "label": "proposals", + "offset": 0, + "slot": "0", + "type": "mapping(uint256 => struct Proposal)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/OptimisticModule.sol b/packages/contracts-bedrock/src/governance/OptimisticModule.sol new file mode 100644 index 00000000000..736b73cbb57 --- /dev/null +++ b/packages/contracts-bedrock/src/governance/OptimisticModule.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IGovernorUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol"; +import { IVotesUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; +import { VotingModule } from "./VotingModule.sol"; + +enum VoteType { + Against, + For, + Abstain +} + +struct ProposalSettings { + uint248 againstThreshold; + bool isRelativeToVotableSupply; +} + +struct Proposal { + address governor; + ProposalSettings settings; +} + +contract OptimisticModule is VotingModule { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error OptimisticModule_WrongProposalId(); + error OptimisticModule_NotOptimisticProposalType(); + error OptimisticModule_OptimisticModuleOnlySignal(); + + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + uint16 public constant PERCENT_DIVISOR = 10_000; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => Proposal) public proposals; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _governor) VotingModule(_governor) { } + + /*////////////////////////////////////////////////////////////// + WRITE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Validate proposal is optimistic and save settings for a new proposal. + /// @param _proposalId The id of the proposal. + /// @param _proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. + function propose(uint256 _proposalId, bytes memory _proposalData, bytes32 _descriptionHash) external override { + _onlyGovernor(); + if (_proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), _proposalData, _descriptionHash)))) { + revert OptimisticModule_WrongProposalId(); + } + + if (proposals[_proposalId].governor != address(0)) { + revert ExistingProposal(); + } + + ProposalSettings memory proposalSettings = abi.decode(_proposalData, (ProposalSettings)); + + uint8 proposalTypeId = IOptimismGovernor(msg.sender).getProposalType(_proposalId); + IProposalTypesConfigurator proposalConfigurator = + IProposalTypesConfigurator(IOptimismGovernor(msg.sender).PROPOSAL_TYPES_CONFIGURATOR()); + IProposalTypesConfigurator.ProposalType memory proposalType = proposalConfigurator.proposalTypes(proposalTypeId); + + if (proposalType.quorum != 0 || proposalType.approvalThreshold != 0) { + revert OptimisticModule_NotOptimisticProposalType(); + } + if ( + proposalSettings.againstThreshold == 0 + || (proposalSettings.isRelativeToVotableSupply && proposalSettings.againstThreshold > PERCENT_DIVISOR) + ) { + revert InvalidParams(); + } + + proposals[_proposalId].governor = msg.sender; + proposals[_proposalId].settings = proposalSettings; + } + + /// @notice Counting logic is skipped. + function _countVote(uint256, address, uint8, uint256, bytes memory) external virtual override { } + + /// @notice Reverts to prevent queue and execute of proposals with optimistic module. + function _formatExecuteParams( + uint256, + bytes memory + ) + public + pure + override + returns (address[] memory, uint256[] memory, bytes[] memory) + { + revert OptimisticModule_OptimisticModuleOnlySignal(); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Return true if `againstVotes` is lower than `againstThreshold`. + /// Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. + /// @param _proposalId The id of the proposal. + function _voteSucceeded(uint256 _proposalId) external view override returns (bool) { + Proposal memory proposal = proposals[_proposalId]; + (uint256 againstVotes,,) = IOptimismGovernor(proposal.governor).proposalVotes(_proposalId); + + uint256 againstThreshold = proposal.settings.againstThreshold; + if (proposal.settings.isRelativeToVotableSupply) { + uint256 snapshotBlock = IGovernorUpgradeable(proposal.governor).proposalSnapshot(_proposalId); + IVotesUpgradeable token = IOptimismGovernor(proposal.governor).token(); + againstThreshold = (token.getPastTotalSupply(snapshotBlock) * againstThreshold) / PERCENT_DIVISOR; + } + + return againstVotes < againstThreshold; + } + + /// @dev Defines the encoding for the expected `proposalData` in `propose`. + /// Encoding: `(ProposalSettings)` + /// Can be used by clients to interact with modules programmatically without prior knowledge + /// on expected types. + function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { + return "((uint248 againstThreshold,bool isRelativeToVotableSupply) proposalSettings)"; + } + + /// @dev Defines the encoding for the expected `params` in `_countVote`. + /// Can be used by clients to interact with modules programmatically without prior knowledge + /// on expected types. + function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { + return ""; + } + + /// @dev See {IGovernor-COUNTING_MODE}. + /// - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. + /// - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo&quorum=against,for,abstain"; + } + + /// @notice Module version. + function version() public pure returns (uint256) { + return 1; + } +} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index ed1fccc64a0..3f3146f00d6 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -17,7 +17,13 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; // Modules -import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; +import { + ProposalSettings as ApprovalProposalSettings, + ProposalOption, + PassingCriteria +} from "src/governance/ApprovalVotingModule.sol"; +import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; +import { VotingModule } from "src/governance/VotingModule.sol"; /// @custom:proxied true /// @title ProposalValidator @@ -67,6 +73,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the criteria value is invalid for council elections (must not exceed options length). error ProposalValidator_InvalidCriteriaValue(); + /// @notice Thrown when the against threshold is invalid (must be > 0 and <= 10000 basis points). + error ProposalValidator_InvalidAgainstThreshold(); + + /// @notice Thrown when an invalid proposal type is provided for upgrade proposals. + error ProposalValidator_InvalidUpgradeProposalType(); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -122,6 +134,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { CouncilBudget } + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @notice The divisor used for percentage calculations in optimistic voting modules. + /// @dev Represents 100% in basis points (10,000 = 100%). + uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -266,6 +286,84 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { transferOwnership(_owner); } + /// @notice Submits a Protocol/Governor Upgrade or Maintenance Upgrade proposal. + /// @param _againstThreshold The percentage that will be used to calculate the fraction of the votable supply + /// that the proposal will need in votes against it to fail. + /// @param _proposalDescription Description of the proposal. + /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). + /// @return proposalHash_ The hash of the submitted proposal. + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + // Validate proposal type is valid for upgrade proposals + if (_proposalType != ProposalType.ProtocolOrGovernorUpgrade && _proposalType != ProposalType.MaintenanceUpgrade) + { + revert ProposalValidator_InvalidUpgradeProposalType(); + } + + // Validate EAS attestation - must be called by owner-approved address + _validateApprovedProposerAttestation(_attestationUid, _proposalType); + + // Validate againstThreshold is non-zero and within bounds for percentage-based thresholds + if (_againstThreshold == 0 || _againstThreshold > OPTIMISTIC_MODULE_PERCENT_DIVISOR) { + revert ProposalValidator_InvalidAgainstThreshold(); + } + + // Create OptimisticModule ProposalSettings with required parameters + OptimisticProposalSettings memory optimisticSettings = OptimisticProposalSettings({ + againstThreshold: _againstThreshold, + isRelativeToVotableSupply: true // MUST always be true + }); + + // Optimistic proposals are signal-only, no execution targets/calldatas needed + bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); + + // Get the optimistic module address from configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Prevent duplicate proposals + if (proposal.proposer != address(0)) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Check if proposal already exists in OptimismGovernor + if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + revert ProposalValidator_ProposalAlreadySubmitted(); + } + + // Store proposal metadata + proposal.proposer = msg.sender; + proposal.proposalType = _proposalType; + + emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); + emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + + // MaintenanceUpgrade proposals move directly to voting (atomic operation) + if (_proposalType == ProposalType.MaintenanceUpgrade) { + proposal.inVoting = true; + + GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + ); + + emit ProposalMovedToVote(proposalHash_, msg.sender); + } + } + /// @notice Submits a Council Member Elections proposal for approval and voting. /// @param _criteriaValue Since the passing criteria type is "TopChoices" this number represents the amount /// of top choices that can pass the voting. @@ -314,7 +412,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval voting settings with TopChoices criteria - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections @@ -421,7 +519,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval voting settings - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index bd4cc5aa594..ede38a009b1 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -28,7 +28,13 @@ import { Proxy } from "src/universal/Proxy.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Modules -import { ProposalSettings, ProposalOption, PassingCriteria } from "src/governance/ApprovalVotingModule.sol"; +import { + ProposalSettings as ApprovalProposalSettings, + ProposalOption, + PassingCriteria +} from "src/governance/ApprovalVotingModule.sol"; +import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; +import { VotingModule } from "src/governance/VotingModule.sol"; // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; @@ -109,7 +115,10 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; - uint8 public constant APPROVAL_VOTING_MODULE_ID = 3; + uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; + uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; + uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; + uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; address owner; address user; @@ -122,6 +131,7 @@ contract ProposalValidator_Init is CommonTest { bytes32 topDelegateAttestation_C; bytes32 topDelegateAttestation_D; address approvalVotingModule; + address optimisticVotingModule; ProposalValidatorForTest public validator; ProposalValidatorForTest public impl; @@ -147,6 +157,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -208,6 +219,34 @@ contract ProposalValidator_Init is CommonTest { _setGovernanceFundProposalType(); _setCouncilBudgetProposalType(); } + + /// @notice Helper function to set ProtocolOrGovernorUpgrade proposal type data. + function _setProtocolOrGovernorUpgradeProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, + ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + }) + ); + } + + /// @notice Helper function to set MaintenanceUpgrade proposal type data. + function _setMaintenanceUpgradeProposalType() internal { + _setProposalTypeData( + ProposalValidator.ProposalType.MaintenanceUpgrade, + ProposalValidator.ProposalTypeData({ + requiredApprovals: 0, // MaintenanceUpgrade moves directly to voting + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + }) + ); + } + + /// @notice Helper function to set both upgrade proposal types. + function _setUpgradeProposalTypes() internal { + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + } /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() @@ -299,7 +338,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -337,7 +376,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - ProposalSettings memory settings = ProposalSettings({ + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(PassingCriteria.TopChoices), budgetToken: address(0), @@ -348,26 +387,36 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(options, settings); } + /// @notice Helper function to construct voting module data for upgrade proposals + function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { + OptimisticProposalSettings memory settings = + OptimisticProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); + + return abi.encode(settings); + } + /// @notice Helper function to setup proposal types configurator mocks - function _setupProposalTypesConfiguratorMocks() internal { - // Mock calls for different proposal type IDs - for (uint8 i = 0; i < 5; i++) { - address moduleAddress = (i == 2 || i == APPROVAL_VOTING_MODULE_ID) ? approvalVotingModule : address(0); - - vm.mockCall( - address(proposalTypesConfigurator), - abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (i)), - abi.encode( - IProposalTypesConfigurator.ProposalType({ - quorum: 100, - approvalThreshold: 100, - name: "Test Proposal Type", - description: "Test Description", - module: moduleAddress - }) - ) - ); + function _mockProposalTypesConfiguratorCall(uint8 _votingModuleId) internal { + address moduleAddress; + if (_votingModuleId == APPROVAL_VOTING_MODULE_ID) { + moduleAddress = approvalVotingModule; + } else if (_votingModuleId == OPTIMISTIC_VOTING_MODULE_ID) { + moduleAddress = optimisticVotingModule; } + + _mockAndExpect( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 100, + approvalThreshold: 100, + name: "Test Proposal Type", + description: "Test Description", + module: moduleAddress + }) + ) + ); } /// @notice Initializes the validator @@ -380,9 +429,6 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Setup mocks - _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest( APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken ); @@ -415,6 +461,7 @@ contract ProposalValidator_Init is CommonTest { user = makeAddr("user"); governor = IOptimismGovernor(makeAddr("governor")); approvalVotingModule = makeAddr("approvalVotingModule"); + optimisticVotingModule = makeAddr("optimisticVotingModule"); // Create schemas vm.prank(owner); @@ -786,13 +833,13 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_ReturnsTrue_succeeds() public { + function test_canApproveProposal_returnsTrue_succeeds() public { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); } - function test_canApproveProposal_ReturnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + function test_canApproveProposal_returnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { // Ensure the attestation uid is not one of the top delegates vm.assume( _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B @@ -955,8 +1002,6 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init uint256[] optionsAmounts; string description; - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - function setUp() public override { super.setUp(); @@ -1016,6 +1061,8 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedHash, votingModuleData); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(proposer); bytes32 proposalHash = validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); @@ -1050,7 +1097,6 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { - // Bound to proposal types that are NOT funding proposals (0, 1, 2) // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -1210,6 +1256,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I abi.encode(0) ); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Submit first proposal vm.prank(user); validator.submitFundingProposal( @@ -1218,6 +1266,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Attempt to submit identical proposal vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(user); validator.submitFundingProposal( FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType @@ -1246,6 +1297,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(user); validator.submitFundingProposal( FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType @@ -1298,14 +1352,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Setup mocks - _setupProposalTypesConfiguratorMocks(); - impl = new ProposalValidatorForTest( APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken ); validator = ProposalValidatorForTest(address(new Proxy(owner))); - // Initialize will be tested manually } function test_initialize_succeeds() public { @@ -1404,8 +1454,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { string proposalDescription; - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - function setUp() public override { super.setUp(); @@ -1452,6 +1500,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedHash, votingModuleData); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid @@ -1543,6 +1593,8 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop abi.encode(0) ); + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Submit first proposal vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( @@ -1555,6 +1607,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Attempt to submit identical proposal should revert vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_B); validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, secondAttestation @@ -1576,6 +1631,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid @@ -1625,3 +1683,330 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } } + +/// @title ProposalValidator_SubmitUpgradeProposal_Test +/// @notice Happy path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { + string proposalDescription; + + function setUp() public override { + super.setUp(); + + _setUpgradeProposalTypes(); + + proposalDescription = "Protocol Upgrade Proposal"; + } + + function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( + uint248 againstThreshold, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, proposer); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertTrue(inVoting, "MaintenanceUpgrade should be in voting immediately"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } + + function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( + uint248 againstThreshold, + address proposer + ) + public + { + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); + + vm.prank(proposer); + bytes32 proposalHash = + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool inVoting, + uint256 approvalCount + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(inVoting, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + } +} + +/// @title ProposalValidator_SubmitUpgradeProposal_TestFail +/// @notice Sad path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { + string proposalDescription; + + function setUp() public override { + super.setUp(); + + _setUpgradeProposalTypes(); + + proposalDescription = "Test upgrade proposal"; + } + + function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) + proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; // 50% + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { + uint248 zeroThreshold = 0; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { + // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR + excessiveThreshold = + uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); + + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(excessiveThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For MaintenanceUpgrade, mock the governor.proposeWithModule call + if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + } + + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + + // Create new attestation for second attempt + bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); + + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_B); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, secondAttestation, proposalType); + } + + function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } + + function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, proposalType), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); + } +} From 9699b3219ca853d1c66f7fe2860c231f02cfc87e Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:10:33 -0300 Subject: [PATCH 49/73] fix: missing attestation validations (#434) * feat: add check for revoked attestations * feat: add valid attestation check in _validateTopDelegateAttestation function * refactor: use helper functions * fix: decoding after attestation existance validation * chore: remove redundant tests * fix: extra prank breaking tests * fix: pre-pr * refactor: improve variable naming --- .../governance/IProposalValidator.sol | 2 + .../snapshots/abi/ProposalValidator.json | 13 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 13 ++- .../test/governance/ProposalValidator.t.sol | 83 +++++++++++++++++-- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 815b321a5df..7ce083e8adb 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -155,6 +155,8 @@ interface IProposalValidator is ISemver { function distributionThreshold() external view returns (uint256); + function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); + function VOTING_TOKEN() external view returns (IGovernanceToken); function GOVERNOR() external view returns (IOptimismGovernor); diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9b982f52282..21b73e82298 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -51,6 +51,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "OPTIMISTIC_MODULE_PERCENT_DIVISOR", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 4333151e9c8..37956c0574c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x5c805e6dba872f7d2f16cb27a1ab7f8b31813e2b79b04d5ea61055bd06f0bf74", - "sourceCodeHash": "0xc1b29b287e1ff7df81aa3bc740fe63fc0203a976d222b1241636b639a478b791" + "initCodeHash": "0xc4efda2929244bf984fd5a3e32b6a8b5fb68622af6b05a31d3e5f7a25cd6bd3b", + "sourceCodeHash": "0x0064ec36b626190c1d2460aac284df6eaddcb51bf03ff60fa45aecad4ea922c6" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3f3146f00d6..c4d59230f1a 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -688,6 +688,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidAttestation(); } + // check if the attestation is revoked + if (attestation.revocationTime != 0) { + revert ProposalValidator_AttestationRevoked(); + } + (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); if ( @@ -712,7 +717,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bool canApprove_) { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); - (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + + // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) + if (attestation.uid == bytes32(0)) { + revert ProposalValidator_InvalidAttestation(); + } // check if the schema is correct if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { @@ -724,6 +733,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); + // check if the attestation includes partial delegation or the recipient is not the caller if (_includePartialDelegation || attestation.recipient != _delegate) { revert ProposalValidator_InvalidAttestation(); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index ede38a009b1..fb0e277a3d3 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -466,7 +466,7 @@ contract ProposalValidator_Init is CommonTest { // Create schemas vm.prank(owner); APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), false + "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), true ); vm.prank(owner); @@ -498,7 +498,7 @@ contract ProposalValidator_Init is CommonTest { data: AttestationRequestData({ recipient: address(0), expirationTime: 0, - revocable: false, + revocable: true, refUID: bytes32(0), data: abi.encode(_delegate, _proposalType), value: 0 @@ -724,6 +724,34 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.prank(topDelegate_A); validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); } + + function test_approveProposal_nonExistentAttestation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + bytes32 _nonExistentAttestationUid + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // Ensure the attestation uid is not one of the valid ones + vm.assume( + _nonExistentAttestationUid != topDelegateAttestation_A + && _nonExistentAttestationUid != topDelegateAttestation_B + && _nonExistentAttestationUid != topDelegateAttestation_C + && _nonExistentAttestationUid != topDelegateAttestation_D + ); + + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + + // Expect the invalid attestation error to be reverted when attestation doesn't exist + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + } } // /// @title ProposalValidator_MoveToVote_Test @@ -848,9 +876,9 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { bool canApprove; // Expect the invalid attestation error to be reverted - vm.expectRevert(); - try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result) { - canApprove = result; + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result_) { + canApprove = result_; } catch { canApprove = false; } @@ -1682,6 +1710,27 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid ); } + + function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { + // Create valid attestation first (make it revocable) + bytes32 revocableAttestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid + ); + } } /// @title ProposalValidator_SubmitUpgradeProposal_Test @@ -2009,4 +2058,28 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.prank(topDelegate_A); validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); } + + function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + uint248 againstThreshold = 5000; + + // Create valid attestation first (make it revocable) + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: attestationUid, value: 0 }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + } } From c9185ff41852fdc1ff15fada0720abd806af1c19 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:58:07 -0300 Subject: [PATCH 50/73] test: add version function test (#438) --- .../test/governance/ProposalValidator.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fb0e277a3d3..2acaae32d85 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -887,6 +887,15 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { } } +/// @title ProposalValidator_Version_Test +/// @notice Tests for the version function +contract ProposalValidator_Version_Test is ProposalValidator_Init { + function test_version_succeeds() public { + string memory versionString = validator.version(); + assertEq(versionString, "1.0.0-beta.1"); + } +} + /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions contract ProposalValidator_Setters_Test is ProposalValidator_Init { From c19485b86fce1fb568669cf620bb2dfefaba680a Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:29:30 +0300 Subject: [PATCH 51/73] feat: move to vote (#435) * refactor: improve construction of approval voting module options * feat: move to vote logic * refactor: code blocks order * fix: proposal types data in initiallize test * feat: add move to vote tests * chore: rename inVoting * fix: semgrep * fix: pre-pr * fix: improve does not exist error * fix: improve error name * fix: test * fix: use const var instead of hardcoding test value * fix: add approved check and test * fix: improve tests --- .../governance/IProposalValidator.sol | 132 +- .../snapshots/abi/ProposalValidator.json | 118 +- .../src/governance/ProposalValidator.sol | 511 ++++++-- .../test/governance/ProposalValidator.t.sol | 1159 ++++++++++++++--- 4 files changed, 1529 insertions(+), 391 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7ce083e8adb..2ba27a28d83 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -10,43 +10,27 @@ import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. interface IProposalValidator is ISemver { + error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); + error ProposalValidator_ProposalAlreadyMovedToVote(); error ProposalValidator_InvalidAttestation(); error ProposalValidator_VotingCycleAlreadySet(); error ProposalValidator_ProposalDoesNotExist(); error ProposalValidator_ProposalTypesDataLengthMismatch(); - error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InvalidFundingProposalType(); error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); error ProposalValidator_AttestationRevoked(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); - error ProposalValidator_InvalidUpgradeProposalType(); error ProposalValidator_InvalidAgainstThreshold(); - - struct ProposalData { - address proposer; - ProposalType proposalType; - bool inVoting; - mapping(address => bool) delegateApprovals; - uint256 approvalCount; - } - - struct ProposalTypeData { - uint256 requiredApprovals; - uint8 proposalVotingModule; - } - - enum ProposalType { - ProtocolOrGovernorUpgrade, - MaintenanceUpgrade, - CouncilMemberElections, - GovernanceFund, - CouncilBudget - } + error ProposalValidator_InvalidUpgradeProposalType(); + error ProposalValidator_InvalidVotingCycle(); + error ProposalValidator_ProposalIdMismatch(); + error ProposalValidator_InvalidProposer(); + error ProposalValidator_InvalidProposal(); event ProposalSubmitted( bytes32 indexed proposalHash, @@ -89,34 +73,49 @@ interface IProposalValidator is ISemver { event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + struct ProposalData { + address proposer; + ProposalType proposalType; + bool movedToVote; + mapping(address => bool) delegateApprovals; + uint256 approvalCount; + uint256 votingCycle; + } - function moveToVote( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description - ) external returns (uint256 governorProposalId_); + struct ProposalTypeData { + uint256 requiredApprovals; + uint8 proposalVotingModule; + } - function setDistributionThreshold(uint256 _distributionThreshold) external; + struct VotingCycleData { + uint256 startingBlock; + uint256 duration; + uint256 votingCycleDistributionLimit; + uint256 movedToVoteTokenCount; + } - function setProposalTypeData( - ProposalType _proposalType, - ProposalTypeData memory _proposalTypeData - ) external; + enum ProposalType { + ProtocolOrGovernorUpgrade, + MaintenanceUpgrade, + CouncilMemberElections, + GovernanceFund, + CouncilBudget + } - function setVotingCycleData( - uint256 _cycleNumber, - uint256 _startBlock, - uint256 _duration, - uint256 _votingCycleDistributionLimit - ) external; + function submitUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription, + bytes32 _attestationUid, + ProposalType _proposalType, + uint256 _votingCycle + ) external returns (bytes32 proposalHash_); function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, string memory _proposalDescription, - bytes32 _attestationUid + bytes32 _attestationUid, + uint256 _votingCycle ) external returns (bytes32 proposalHash_); function submitFundingProposal( @@ -125,16 +124,48 @@ interface IProposalValidator is ISemver { address[] memory _optionsRecipients, uint256[] memory _optionsAmounts, string memory _description, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_); - function submitUpgradeProposal( + function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + + function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, - string memory _proposalDescription, - bytes32 _attestationUid, + string memory _proposalDescription + ) external returns (bytes32 proposalHash_); + + function moveToVoteCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + string memory _proposalDescription + ) external returns (bytes32 proposalHash_); + + function moveToVoteFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, ProposalType _proposalType ) external returns (bytes32 proposalHash_); + function setVotingCycleData( + uint256 _cycleNumber, + uint256 _startBlock, + uint256 _duration, + uint256 _votingCycleDistributionLimit + ) external; + + function setDistributionThreshold(uint256 _distributionThreshold) external; + + function setProposalTypeData( + ProposalType _proposalType, + ProposalTypeData memory _proposalTypeData + ) external; + function initialize( address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, @@ -151,12 +182,8 @@ interface IProposalValidator is ISemver { function transferOwnership(address newOwner) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); - function distributionThreshold() external view returns (uint256); - function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function VOTING_TOKEN() external view returns (IGovernanceToken); function GOVERNOR() external view returns (IOptimismGovernor); @@ -169,6 +196,8 @@ interface IProposalValidator is ISemver { function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); + function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); @@ -176,7 +205,8 @@ interface IProposalValidator is ISemver { function votingCycles(uint256) external view returns ( uint256 startingBlock, uint256 duration, - uint256 votingCycleDistributionLimit + uint256 votingCycleDistributionLimit, + uint256 movedToVoteTokenCount ); function __constructor__( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 21b73e82298..4af4cf80adf 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -225,33 +225,96 @@ }, { "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, + { + "internalType": "string", + "name": "_proposalDescription", + "type": "string" + } + ], + "name": "moveToVoteCouncilMemberElectionsProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "_criteriaValue", + "type": "uint128" + }, + { + "internalType": "string[]", + "name": "_optionsDescriptions", + "type": "string[]" + }, { "internalType": "address[]", - "name": "_targets", + "name": "_optionsRecipients", "type": "address[]" }, { "internalType": "uint256[]", - "name": "_values", + "name": "_optionsAmounts", "type": "uint256[]" }, { - "internalType": "bytes[]", - "name": "_calldatas", - "type": "bytes[]" + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "enum ProposalValidator.ProposalType", + "name": "_proposalType", + "type": "uint8" + } + ], + "name": "moveToVoteFundingProposal", + "outputs": [ + { + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint248", + "name": "_againstThreshold", + "type": "uint248" }, { "internalType": "string", - "name": "_description", + "name": "_proposalDescription", "type": "string" } ], - "name": "moveToVote", + "name": "moveToVoteProtocolOrGovernorUpgradeProposal", "outputs": [ { - "internalType": "uint256", - "name": "governorProposalId_", - "type": "uint256" + "internalType": "bytes32", + "name": "proposalHash_", + "type": "bytes32" } ], "stateMutability": "nonpayable", @@ -406,6 +469,11 @@ "internalType": "bytes32", "name": "_attestationUid", "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitCouncilMemberElectionsProposal", @@ -450,6 +518,11 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "_proposalType", "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitFundingProposal", @@ -484,6 +557,11 @@ "internalType": "enum ProposalValidator.ProposalType", "name": "_proposalType", "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_votingCycle", + "type": "uint256" } ], "name": "submitUpgradeProposal", @@ -547,6 +625,11 @@ "internalType": "uint256", "name": "votingCycleDistributionLimit", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "movedToVoteTokenCount", + "type": "uint256" } ], "stateMutability": "view", @@ -791,11 +874,21 @@ "name": "ProposalValidator_InvalidUpgradeProposalType", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidVotingCycle", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalAlreadyMovedToVote", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadySubmitted", @@ -806,6 +899,11 @@ "name": "ProposalValidator_ProposalDoesNotExist", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_ProposalIdMismatch", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalTypesDataLengthMismatch", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c4d59230f1a..bd8167e57a0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -43,6 +43,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when attempting to move a proposal to vote that is already in voting. error ProposalValidator_ProposalAlreadySubmitted(); + /// @notice Thrown when attempting to move a proposal to vote that is already in voting. + error ProposalValidator_ProposalAlreadyMovedToVote(); + /// @notice Thrown when an invalid attestation is provided for a proposal. error ProposalValidator_InvalidAttestation(); @@ -79,6 +82,65 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an invalid proposal type is provided for upgrade proposals. error ProposalValidator_InvalidUpgradeProposalType(); + /// @notice Thrown when the trying to move a proposal to vote outside of the accepted voting cycle. + error ProposalValidator_InvalidVotingCycle(); + + /// @notice Thrown when the proposalId returned by the Governor is not the same as the proposalHash. + error ProposalValidator_ProposalIdMismatch(); + + /// @notice Thrown when the caller is not the proposer. + error ProposalValidator_InvalidProposer(); + + /// @notice Thrown when the proposal is invalid trying to move to vote. + error ProposalValidator_InvalidProposal(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new proposal is submitted. + /// @param proposalHash The hash of the submitted proposal. + /// @param proposer The address that submitted the proposal. + /// @param description Description of the proposal. + /// @param proposalType Type of the proposal. + event ProposalSubmitted( + bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + ); + + /// @notice Emitted when a delegate approves a proposal. + /// @param proposalHash The hash of the approved proposal. + /// @param approver The address of the delegate who approved the proposal. + event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + + /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. + /// @param proposalHash The hash of the proposal moved to vote. + /// @param executor The address that executed the move to vote. + event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + + /// @notice Emitted when the voting cycle data is set. + /// @param cycleNumber The number of the voting cycle. + /// @param startBlock The block number of the starting block of the voting cycle. + /// @param duration The duration of the voting cycle. + /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. + event VotingCycleDataSet( + uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + ); + + /// @notice Emitted when the distribution threshold is set. + /// @param newDistributionThreshold The new distribution threshold. + event DistributionThresholdSet(uint256 newDistributionThreshold); + + /// @notice Emitted when the proposal type data is set. + /// @param proposalType The type of proposal. + /// @param requiredApprovals The required number of approvals. + /// @param proposalVotingModule The proposal type ID. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + + /// @notice Emitted with ProposalSubmitted event. + /// @param proposalHash The hash of the submitted proposal. + /// @param encodedVotingModuleData The encoded voting module data. + event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + /*////////////////////////////////////////////////////////////// STRUCTS //////////////////////////////////////////////////////////////*/ @@ -86,15 +148,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing proposal information. /// @param proposer The address that submitted the proposal. /// @param proposalType Type of the proposal from the ProposalType enum. - /// @param inVoting Whether the proposal has been moved to the voting phase. + /// @param movedToVote Whether the proposal has been proposed to the Governor for voting. /// @param delegateApprovals Mapping of delegate addresses to their approval status. /// @param approvalCount Number of approvals received so far. + /// @param votingCycle The voting cycle number the proposal is targetted for. struct ProposalData { address proposer; ProposalType proposalType; - bool inVoting; + bool movedToVote; mapping(address => bool) delegateApprovals; uint256 approvalCount; + uint256 votingCycle; } /// @notice Struct for storing explicit data for each proposal type. @@ -110,10 +174,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param startingBlock The block number of the starting block of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. + /// @param movedToVoteTokenCount The total amount of tokens to possibly be distributed in the voting cycle. struct VotingCycleData { uint256 startingBlock; uint256 duration; uint256 votingCycleDistributionLimit; + uint256 movedToVoteTokenCount; } /*////////////////////////////////////////////////////////////// @@ -143,52 +209,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; /*////////////////////////////////////////////////////////////// - EVENTS + STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a new proposal is submitted. - /// @param proposalHash The hash of the submitted proposal. - /// @param proposer The address that submitted the proposal. - /// @param description Description of the proposal. - /// @param proposalType Type of the proposal. - event ProposalSubmitted( - bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType - ); - - /// @notice Emitted when a delegate approves a proposal. - /// @param proposalHash The hash of the approved proposal. - /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); - - /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalHash The hash of the proposal moved to vote. - /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); - - /// @notice Emitted when the voting cycle data is set. - /// @param cycleNumber The number of the voting cycle. - /// @param startBlock The block number of the starting block of the voting cycle. - /// @param duration The duration of the voting cycle. - /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. - event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit - ); - - /// @notice Emitted when the distribution threshold is set. - /// @param newDistributionThreshold The new distribution threshold. - event DistributionThresholdSet(uint256 newDistributionThreshold); - - /// @notice Emitted when the proposal type data is set. - /// @param proposalType The type of proposal. - /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal type ID. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); - - /// @notice Emitted with ProposalSubmitted event. - /// @param proposalHash The hash of the submitted proposal. - /// @param encodedVotingModuleData The encoded voting module data. - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); - /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } @@ -292,12 +315,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, bytes32 _attestationUid, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -348,13 +373,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) if (_proposalType == ProposalType.MaintenanceUpgrade) { - proposal.inVoting = true; + proposal.movedToVote = true; GOVERNOR.proposeWithModule( VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) @@ -370,12 +396,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionDescriptions The strings of the different options that can be voted. /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, string memory _proposalDescription, - bytes32 _attestationUid + bytes32 _attestationUid, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -394,22 +422,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidCriteriaValue(); } - ProposalOption[] memory options = new ProposalOption[](optionsLength); - - // Build proposal options without any execution calls (elections don't execute operations) - for (uint256 i = 0; i < optionsLength; i++) { - address[] memory targets = new address[](0); - uint256[] memory values = new uint256[](0); - bytes[] memory calldatas = new bytes[](0); - - options[i] = ProposalOption({ - budgetTokensSpent: 0, // No tokens spent for elections - targets: targets, - values: values, - calldatas: calldatas, - description: _optionDescriptions[i] - }); - } + // Build proposal options (elections don't execute operations) + (ProposalOption[] memory options,) = + _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria ApprovalProposalSettings memory settings = ApprovalProposalSettings({ @@ -446,6 +461,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = ProposalType.CouncilMemberElections; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -463,6 +479,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @param _votingCycle The voting cycle number the proposal is targetted for. /// @return proposalHash_ The hash of the submitted proposal. function submitFundingProposal( uint128 _criteriaValue, @@ -470,7 +487,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address[] memory _optionsRecipients, uint256[] memory _optionsAmounts, string memory _description, - ProposalType _proposalType + ProposalType _proposalType, + uint256 _votingCycle ) external returns (bytes32 proposalHash_) @@ -491,32 +509,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidOptionsLength(); } - ProposalOption[] memory options = new ProposalOption[](optionsLength); - uint256 totalBudget = 0; - - // Check amounts, build options, and calculate total budget in single loop - for (uint256 i = 0; i < optionsLength; i++) { - if (_optionsAmounts[i] > distributionThreshold) { - revert ProposalValidator_ExceedsDistributionThreshold(); - } - - address[] memory targets = new address[](1); - uint256[] memory values = new uint256[](1); - bytes[] memory calldatas = new bytes[](1); - - targets[0] = Predeploys.GOVERNANCE_TOKEN; - calldatas[0] = abi.encodeCall(IERC20.transfer, (_optionsRecipients[i], _optionsAmounts[i])); - - options[i] = ProposalOption({ - budgetTokensSpent: _optionsAmounts[i], - targets: targets, - values: values, - calldatas: calldatas, - description: _optionsDescriptions[i] - }); - - totalBudget += _optionsAmounts[i]; - } + // Build proposal options with funding execution data + (ProposalOption[] memory options, uint256 totalBudget) = + _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings ApprovalProposalSettings memory settings = ApprovalProposalSettings({ @@ -551,6 +546,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = msg.sender; proposal.proposalType = _proposalType; + proposal.votingCycle = _votingCycle; emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -583,53 +579,263 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalApproved(_proposalHash, _delegate); } - /// @notice Move a proposal to voting phase after sufficient delegate approvals - /// @param _targets Target addresses for proposal calls - /// @param _values ETH values for proposal calls - /// @param _calldatas Function data for proposal calls - /// @param _description Description of the proposal - /// @return governorProposalId_ The proposal ID in the governor contract - function moveToVote( - address[] memory _targets, - uint256[] memory _values, - bytes[] memory _calldatas, - string memory _description + /// @notice Checks if a delegate can approve a proposal. + /// @dev Helper function for UI integration. + /// @param _attestationUid The UID of the attestation to check. + /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. + function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + } + + /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. + /// @param _againstThreshold The threshold for the proposal to be against the total supply. + /// @param _proposalDescription Description of the proposal. + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteProtocolOrGovernorUpgradeProposal( + uint248 _againstThreshold, + string memory _proposalDescription ) external - returns (uint256 governorProposalId_) + returns (bytes32 proposalHash_) { - // Verify that the provided data matches the proposalHash - bytes32 _proposalHash = bytes32(0); // TODO: Implement hashProposalWithModule + // Configure optimistic proposal settings + OptimisticProposalSettings memory settings = + OptimisticProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); - ProposalData storage proposal = _proposals[_proposalHash]; + bytes memory proposalVotingModuleData = abi.encode(settings); - if (proposal.proposer == address(0)) { - revert ProposalValidator_ProposalDoesNotExist(); + // Get the module address from the configurator + ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; + address votingModule = proposalTypesConfigurator.proposalTypes( + proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule + ).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist and be valid + if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if the caller is the proposer + if (proposal.proposer != _msgSender()) { + revert ProposalValidator_InvalidProposer(); } - ProposalTypeData memory proposalTypeData = proposalTypesData[proposal.proposalType]; - if (proposal.approvalCount < proposalTypeData.requiredApprovals) { + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } - if (proposal.inVoting) { - revert ProposalValidator_ProposalAlreadySubmitted(); + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); } - proposal.inVoting = true; + proposal.movedToVote = true; - governorProposalId_ = - GOVERNOR.propose(_targets, _values, _calldatas, _description, uint8(proposal.proposalType)); + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), + proposalVotingModuleData, + _proposalDescription, + uint8(proposalType) + ); - emit ProposalMovedToVote(_proposalHash, msg.sender); + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); } - /// @notice Checks if a delegate can approve a proposal. - /// @dev Helper function for UI integration. - /// @param _attestationUid The UID of the attestation to check. - /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + /// @notice Moves a council member elections proposal to vote by proposing it on the Governor. + /// @param _criteriaValue The number of top choices that can pass the voting. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _proposalDescription Description of the proposal. + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteCouncilMemberElectionsProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + string memory _proposalDescription + ) + external + returns (bytes32 proposalHash_) + { + // Configure approval module options + (ProposalOption[] memory options,) = + _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); + + // Configure approval module settings + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + maxApprovals: uint8(_optionsDescriptions.length), + criteria: uint8(PassingCriteria.TopChoices), + budgetToken: address(0), + criteriaValue: _criteriaValue, + budgetAmount: 0 + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + ProposalType proposalType = ProposalType.CouncilMemberElections; + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = + _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist and be valid + if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if the caller is the proposer + if (proposal.proposer != _msgSender()) { + revert ProposalValidator_InvalidProposer(); + } + + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { + revert ProposalValidator_InsufficientApprovals(); + } + + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + + // Check if the voting cycle is valid + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + // TODO: is + duration correct? + if ( + votingCycleData.startingBlock > block.number + || votingCycleData.startingBlock + votingCycleData.duration < block.number + ) { + revert ProposalValidator_InvalidVotingCycle(); + } + + proposal.movedToVote = true; + + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + ); + + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); + } + + /// @notice Moves a funding proposal to vote by proposing it on the Governor. + /// @dev For UI integration: Frontend interfaces should present this as a percentage input to users (e.g., "25%"), + /// then convert to the absolute vote count by calculating: (percentage / 100) * total_votable_supply. + /// Direct contract callers must provide the absolute number of votes required for passage. + /// @param _criteriaValue The absolute number of votes required for the proposal to pass. This represents the + /// threshold that must be met or exceeded for any option to be considered successful. + /// @param _optionsDescriptions The strings of the different options that can be voted. + /// @param _optionsRecipients An address for each option to transfer funds to in case the option passes the voting. + /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. + /// @param _description Description of the proposal. + /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). + /// @return proposalHash_ The hash of the submitted proposal. + function moveToVoteFundingProposal( + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _description, + ProposalType _proposalType + ) + external + returns (bytes32 proposalHash_) + { + uint256 optionsLength = _optionsDescriptions.length; + // Only funding proposal types can use this function + if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { + revert ProposalValidator_InvalidFundingProposalType(); + } + + // Configure approval module options + (ProposalOption[] memory options, uint256 totalBudget) = + _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); + + // Configure approval module settings + ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + maxApprovals: uint8(optionsLength), + criteria: uint8(PassingCriteria.Threshold), + budgetToken: Predeploys.GOVERNANCE_TOKEN, + criteriaValue: _criteriaValue, + budgetAmount: uint128(totalBudget) + }); + + bytes memory proposalVotingModuleData = abi.encode(options, settings); + + // Get the module address from the configurator + address votingModule = + proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + + // Generate unique proposal hash + proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + + ProposalData storage proposal = _proposals[proposalHash_]; + + // Proposal must exist + if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { + revert ProposalValidator_InvalidProposal(); + } + + // Check if proposal has enough approvals + if (proposal.approvalCount < proposalTypesData[_proposalType].requiredApprovals) { + revert ProposalValidator_InsufficientApprovals(); + } + + // Check if proposal is already in voting + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + + // Check if proposal can be moved to vote + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + // TODO: is + duration correct? + if ( + votingCycleData.startingBlock > block.number + || votingCycleData.startingBlock + votingCycleData.duration < block.number + ) { + revert ProposalValidator_InvalidVotingCycle(); + } + + // Check if total budget is within the voting cycle distribution limit + if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + + // Move proposal to vote + proposal.movedToVote = true; + votingCycles[proposal.votingCycle].movedToVoteTokenCount += totalBudget; + + // Propose with module on the Governor + uint256 proposalId = GOVERNOR.proposeWithModule( + VotingModule(votingModule), proposalVotingModuleData, _description, uint8(_proposalType) + ); + + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(proposalHash_, _msgSender()); } /// @notice Sets the data of a voting cycle. @@ -743,6 +949,62 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { canApprove_ = true; } + /// @notice Internal function to build proposal options with optional execution data. + /// @param _optionDescriptions The strings of the different options that can be voted. + /// @param _recipients An address for each option to transfer funds to (empty for non-funding proposals). + /// @param _amounts The amount to transfer for each option (empty for non-funding proposals). + /// @return options_ The built proposal options. + /// @return totalBudget_ The total budget amount (sum of all amounts, 0 for non-funding proposals). + function _buildApprovalModuleOptions( + string[] memory _optionDescriptions, + address[] memory _recipients, + uint256[] memory _amounts + ) + internal + view + returns (ProposalOption[] memory options_, uint256 totalBudget_) + { + uint256 optionsLength = _optionDescriptions.length; + options_ = new ProposalOption[](optionsLength); + + for (uint256 i = 0; i < optionsLength; i++) { + address[] memory targets; + uint256[] memory values; + bytes[] memory calldatas; + uint256 budgetTokensSpent; + + // Check if this is a funding proposal (has recipients and amounts) + if (_recipients.length > 0 && _amounts.length > 0) { + // Validate amount doesn't exceed distribution threshold + if (_amounts[i] > distributionThreshold) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } + targets = new address[](1); + values = new uint256[](1); + calldatas = new bytes[](1); + + targets[0] = Predeploys.GOVERNANCE_TOKEN; + calldatas[0] = abi.encodeCall(IERC20.transfer, (_recipients[i], _amounts[i])); + budgetTokensSpent = _amounts[i]; + totalBudget_ += _amounts[i]; + } else { + // Non-funding proposals have no execution data + targets = new address[](0); + values = new uint256[](0); + calldatas = new bytes[](0); + budgetTokensSpent = 0; + } + + options_[i] = ProposalOption({ + budgetTokensSpent: budgetTokensSpent, + targets: targets, + values: values, + calldatas: calldatas, + description: _optionDescriptions[i] + }); + } + } + /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. @@ -780,7 +1042,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingCycles[_cycleNumber] = VotingCycleData({ startingBlock: _startBlock, duration: _duration, - votingCycleDistributionLimit: _votingCycleDistributionLimit + votingCycleDistributionLimit: _votingCycleDistributionLimit, + movedToVoteTokenCount: 0 }); emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 2acaae32d85..7721ac54f9e 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -73,25 +73,35 @@ contract ProposalValidatorForTest is ProposalValidator { function getProposalData(bytes32 _proposalHash) public view - returns (address proposer_, ProposalType proposalType_, bool inVoting_, uint256 approvalCount_) + returns ( + address proposer_, + ProposalType proposalType_, + bool movedToVote_, + uint256 approvalCount_, + uint256 votingCycle_ + ) { ProposalData storage proposal = _proposals[_proposalHash]; - return (proposal.proposer, proposal.proposalType, proposal.inVoting, proposal.approvalCount); + return ( + proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle + ); } function setProposalData( bytes32 _proposalHash, address _proposer, ProposalType _proposalType, - bool _inVoting, - uint256 _approvalCount + bool _movedToVote, + uint256 _approvalCount, + uint256 _votingCycle ) public { _proposals[_proposalHash].proposer = _proposer; _proposals[_proposalHash].proposalType = _proposalType; - _proposals[_proposalHash].inVoting = _inVoting; + _proposals[_proposalHash].movedToVote = _movedToVote; _proposals[_proposalHash].approvalCount = _approvalCount; + _proposals[_proposalHash].votingCycle = _votingCycle; } function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { @@ -112,9 +122,9 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_BLOCK = 1000000; uint256 public constant DURATION = 100; - uint256 public constant DISTRIBUTION_LIMIT = 10000 ether; + uint256 public constant DISTRIBUTION_LIMIT = 20000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; - uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 4; + uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 1; uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; @@ -130,6 +140,7 @@ contract ProposalValidator_Init is CommonTest { bytes32 topDelegateAttestation_B; bytes32 topDelegateAttestation_C; bytes32 topDelegateAttestation_D; + address approvedProposer = makeAddr("approvedProposer"); address approvalVotingModule; address optimisticVotingModule; @@ -277,22 +288,27 @@ contract ProposalValidator_Init is CommonTest { proposalTypes[4] = ProposalValidator.ProposalType.CouncilBudget; ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](5); + // ProtocolOrGovernorUpgrade proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID }); + // MaintenanceUpgrade proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 + requiredApprovals: 0, + proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID }); + // CouncilMemberElections proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 2 + proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); + // GovernanceFund proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, proposalVotingModule: APPROVAL_VOTING_MODULE_ID }); + // CouncilBudget proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, proposalVotingModule: APPROVAL_VOTING_MODULE_ID @@ -301,7 +317,7 @@ contract ProposalValidator_Init is CommonTest { return (proposalTypes, proposalTypesData); } - function _constructVotingModuleData( + function _constructFundingVotingModuleData( string[] memory descriptions, address[] memory recipients, uint256[] memory amounts, @@ -395,6 +411,65 @@ contract ProposalValidator_Init is CommonTest { return abi.encode(settings); } + /// @notice Helper function to create a proposal for move to vote + function _createUpgradeProposalForMoveToVote( + address proposer, + uint248 againstThreshold, + string memory proposalDescription + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + // Calculate expected proposal hash + votingModuleData_ = _constructOptimisticVotingModuleData(againstThreshold); + proposalHash_ = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + // 1 vote as default for being able to move to vote + validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + + /// @notice Helper function to create a proposal for move to vote for council elections + function _createCouncilElectionProposalForMoveToVote( + address proposer, + uint128 criteriaValue, + string[] memory optionsDescriptions, + string memory proposalDescription + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + votingModuleData_ = _constructCouncilElectionVotingModuleData(optionsDescriptions, criteriaValue); + proposalHash_ = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + + /// @notice Helper function to create a proposal for move to vote for a funding proposal type + function _createFundingProposalForMoveToVote( + address proposer, + uint128 criteriaValue, + string[] memory optionsDescriptions, + address[] memory optionsRecipients, + uint256[] memory optionsAmounts, + string memory proposalDescription, + ProposalValidator.ProposalType proposalType + ) + internal + returns (bytes32 proposalHash_, bytes memory votingModuleData_) + { + votingModuleData_ = + _constructFundingVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + proposalHash_ = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + ); + + validator.setProposalData(proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + } + /// @notice Helper function to setup proposal types configurator mocks function _mockProposalTypesConfiguratorCall(uint8 _votingModuleId) internal { address moduleAddress; @@ -524,26 +599,6 @@ contract ProposalValidator_Init is CommonTest { }) ); } - - /// @notice Helper to create a standard proposal setup - function _createProposalSetup() - internal - view - returns ( - address[] memory targets_, - uint256[] memory values_, - bytes[] memory calldatas_, - string memory description_ - ) - { - targets_ = new address[](1); - targets_[0] = address(0); - values_ = new uint256[](1); - values_[0] = 0; - calldatas_ = new bytes[](1); - calldatas_[0] = bytes(""); - description_ = "Test proposal"; - } } /// @title ProposalValidator_ApproveProposal_Test @@ -558,7 +613,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect event to be emitted when approving vm.expectEmit(address(validator)); @@ -571,7 +626,7 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { // Check that the proposal data has been updated assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - (,,, uint256 approvalCount) = validator.getProposalData(_proposalHash); + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); assertEq(approvalCount, 1); } } @@ -600,7 +655,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal as already approved by the top delegate validator.mockApproveProposal(_proposalHash, topDelegate_A); @@ -637,7 +692,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); @@ -649,7 +704,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // revoke the attestation vm.prank(owner); @@ -682,7 +737,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -701,7 +756,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // create an attestation with partial delegation vm.prank(owner); @@ -745,7 +800,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0); + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -754,130 +809,760 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { } } -// /// @title ProposalValidator_MoveToVote_Test -// /// @notice Happy path tests for moveToVote function -// contract ProposalValidator_MoveToVote_Test is ProposalValidator_Init { -// address[] targets; -// uint256[] values; -// bytes[] calldatas; -// string description; -// ProposalValidator.ProposalType proposalType; -// uint8 proposalVotingModule; - -// function setUp() public override { -// super.setUp(); - -// (targets, values, calldatas, description) = _createProposalSetup(); - -// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; -// proposalVotingModule = 0; -// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - -// /* vm.prank(topDelegate_A); */ -// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented - -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); -// _approveProposal(topDelegate_D, proposalHash); -// } - -// function test_moveToVote_succeeds() public { -// _mockAndExpect( -// address(governor), -// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, -// proposalVotingModule)), -// abi.encode(1) -// ); - -// // Expect the ProposalMovedToVote event to be emitted -// vm.expectEmit(address(validator)); -// emit ProposalMovedToVote(proposalHash, owner); - -// vm.prank(owner); -// uint256 governorProposalId = validator.moveToVote(targets, values, calldatas, description); - -// assertEq(governorProposalId, 1); -// } -// } - -// /// @title ProposalValidator_MoveToVote_TestFail -// /// @notice Sad path tests for moveToVote function -// contract ProposalValidator_MoveToVote_TestFail is ProposalValidator_Init { -// address[] targets; -// uint256[] values; -// bytes[] calldatas; -// string description; -// ProposalValidator.ProposalType proposalType; -// uint8 proposalVotingModule; - -// function setUp() public override { -// super.setUp(); - -// (targets, values, calldatas, description) = _createProposalSetup(); - -// proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; -// proposalVotingModule = 0; -// bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - -// /* vm.prank(topDelegate_A); */ -// proposalHash = bytes32(0); // TODO: Implement after submitFundingProposal is implemented -// } - -// function test_moveToVote_insufficientApprovals_reverts() public { -// // Only approve with 3 delegates (need 4) -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); - -// vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); -// } - -// function test_moveToVote_alreadyProposed_reverts() public { -// // Approve with all 4 delegates -// _approveProposal(topDelegate_A, proposalHash); -// _approveProposal(topDelegate_B, proposalHash); -// _approveProposal(topDelegate_C, proposalHash); -// _approveProposal(topDelegate_D, proposalHash); - -// _mockAndExpect( -// address(governor), -// abi.encodeCall(IOptimismGovernor.propose, (targets, values, calldatas, description, -// proposalVotingModule)), -// abi.encode(1) -// ); - -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); - -// vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); -// vm.prank(owner); -// validator.moveToVote(targets, values, calldatas, description); -// } -// } +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( + approvedProposer, againstThreshold, proposalDescription + ); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( + approvedProposer, againstThreshold, proposalDescription + ); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) + public + { + vm.assume(_caller != approvedProposer); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts( + uint248 _againstThreshold + ) + public + { + // This will generate a different proposal hash which will make the proposal type wrong + vm.assume(_againstThreshold != againstThreshold); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } +} + +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + bytes32 expectedHash; + bytes votingModuleData; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + proposalDescription + ); + } + + function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + bytes32 expectedHash; + bytes votingModuleData; + + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + proposalDescription + ); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) + public + { + vm.assume(_caller != approvedProposer); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { + // This will generate a different proposal hash which will make the proposal type wrong + uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(block.number + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } +} + +contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions = new string[](2); + address[] optionsRecipients = new address[](2); + uint256[] optionsAmounts = new uint256[](2); + bytes32 expectedGovernanceFundHash; + bytes32 expectedCouncilBudgetHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; + + function setUp() public override { + super.setUp(); + + // Create option descriptions for the proposals + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + + // Create option recipients for the proposals + optionsRecipients[0] = makeAddr("optionRecipient1"); + optionsRecipients[1] = makeAddr("optionRecipient2"); + + // Create option amounts for the proposals + optionsAmounts[0] = 100 ether; + optionsAmounts[1] = 200 ether; + + // Create one proposal for each type + (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } + + function test_moveToVoteFundingProposal_governanceFund_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + governanceFundVotingModuleData, + governanceFundProposalDescription, + uint8(governanceFundProposalType) + ) + ), + abi.encode(uint256(expectedGovernanceFundHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } + + function test_moveToVoteFundingProposal_councilBudget_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + councilBudgetVotingModuleData, + councilBudgetProposalDescription, + uint8(councilBudgetProposalType) + ) + ), + abi.encode(uint256(expectedCouncilBudgetHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } +} + +contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + bytes32 governanceFundExpectedHash; + bytes32 councilBudgetExpectedHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; + + function setUp() public override { + super.setUp(); + + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } + + function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( + uint8 _proposalTypeValue, + string memory _proposalDescription + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposal_reverts( + uint8 _proposalTypeValue, + uint128 _criteriaValue + ) + public + { + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // not find the proposal + vm.assume(_criteriaValue != criteriaValue); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( + uint8 _wrongProposalTypeValue, + uint8 _validProposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); + + _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); + + string memory proposalDescription; + if (validProposalType == governanceFundProposalType) { + // Set proposal data proposal type to a different value + validator.setProposalData(governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data proposal type to a different value + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + proposalDescription, + validProposalType + ); + } + + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data movedToVote to true + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data movedToVote to true + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(START_BLOCK + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + uint8 _proposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Set the first option amount to exceed the distribution threshold + optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + string[] memory _optionsDescriptions = new string[](3); + address[] memory _optionsRecipients = new address[](3); + uint256[] memory _optionsAmounts = new uint256[](3); + + _optionsDescriptions[0] = "Option 1"; + _optionsDescriptions[1] = "Option 2"; + _optionsDescriptions[2] = "Option 3"; + + _optionsRecipients[0] = makeAddr("optionRecipient1"); + _optionsRecipients[1] = makeAddr("optionRecipient2"); + _optionsRecipients[2] = makeAddr("optionRecipient3"); + + _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; + + _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + _optionsDescriptions, + _optionsRecipients, + _optionsAmounts, + proposalDescription, + proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + vm.roll(START_BLOCK + 1); + validator.moveToVoteFundingProposal( + criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( + uint8 _proposalTypeValue, + bytes32 _randomHash + ) + public + { + vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + bytes memory votingModuleData; + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + votingModuleData = governanceFundVotingModuleData; + proposalDescription = governanceFundProposalDescription; + } else { + votingModuleData = councilBudgetVotingModuleData; + proposalDescription = councilBudgetProposalDescription; + } + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } +} /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnsTrue_succeeds() public { + function test_canApproveProposal_returnTrue_succeeds() public { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); } - function test_canApproveProposal_returnsFalse_succeeds(bytes32 _attestationUid, address _delegate) public { + function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { // Ensure the attestation uid is not one of the top delegates vm.assume( - _attestationUid != topDelegateAttestation_A && _attestationUid != topDelegateAttestation_B - && _attestationUid != topDelegateAttestation_C && _attestationUid != topDelegateAttestation_D + attestationUid != topDelegateAttestation_A && attestationUid != topDelegateAttestation_B + && attestationUid != topDelegateAttestation_C && attestationUid != topDelegateAttestation_D ); bool canApprove; // Expect the invalid attestation error to be reverted - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); - try validator.canApproveProposal(_attestationUid, _delegate) returns (bool result_) { + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { canApprove = result_; } catch { canApprove = false; @@ -916,12 +1601,17 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - (uint256 actualStartBlock, uint256 actualDuration, uint256 actualDistributionLimit) = - validator.votingCycles(cycleNumber); + ( + uint256 actualStartBlock, + uint256 actualDuration, + uint256 actualDistributionLimit, + uint256 actualMovedToVoteTokenCount + ) = validator.votingCycles(cycleNumber); assertEq(actualStartBlock, startBlock); assertEq(actualDuration, duration); assertEq(actualDistributionLimit, distributionLimit); + assertEq(actualMovedToVoteTokenCount, 0); } function test_setVotingCycleData_notOwner_reverts() public { @@ -1079,7 +1769,8 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init } // Calculate expected proposal hash - bytes memory votingModuleData = _constructVotingModuleData(descriptions, recipients, amounts, criteriaValue); + bytes memory votingModuleData = + _constructFundingVotingModuleData(descriptions, recipients, amounts, criteriaValue); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1101,8 +1792,9 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(proposer); - bytes32 proposalHash = - validator.submitFundingProposal(criteriaValue, descriptions, recipients, amounts, description, proposalType); + bytes32 proposalHash = validator.submitFundingProposal( + criteriaValue, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1110,14 +1802,16 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(inVoting, "Proposal should not be in voting yet"); + assertFalse(movedToVote, "Proposal should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1144,7 +1838,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1177,7 +1871,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingRecipients, matchingAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1210,7 +1905,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedRecipients, matchingAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1243,7 +1939,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingRecipients, mismatchedAmounts, description, - proposalType + proposalType, + CYCLE_NUMBER ); } @@ -1268,7 +1965,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1282,7 +1979,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); + _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1298,7 +1995,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); // Attempt to submit identical proposal @@ -1308,7 +2005,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1322,7 +2019,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal hash bytes memory votingModuleData = - _constructVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); + _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); bytes32 expectedHash = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); @@ -1339,7 +2036,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } @@ -1354,7 +2051,13 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, emptyDescriptions, emptyRecipients, emptyAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, + emptyDescriptions, + emptyRecipients, + emptyAmounts, + description, + proposalType, + CYCLE_NUMBER ); } @@ -1376,7 +2079,13 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, tooManyDescriptions, tooManyRecipients, tooManyAmounts, description, proposalType + FUNDING_CRITERIA_VALUE, + tooManyDescriptions, + tooManyRecipients, + tooManyAmounts, + description, + proposalType, + CYCLE_NUMBER ); } } @@ -1425,24 +2134,32 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { assertEq(validator.owner(), owner); // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit) = validator.votingCycles(CYCLE_NUMBER); + (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + validator.votingCycles(CYCLE_NUMBER); assertEq(startBlock, START_BLOCK); assertEq(duration, DURATION); assertEq(distributionLimit, DISTRIBUTION_LIMIT); + assertEq(movedToVoteTokenCount, 0); // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); - assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { + assertEq(requiredApprovals, 0); + } else { + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + } - // Both GovernanceFund and CouncilBudget use APPROVAL_VOTING_MODULE_ID + // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID if ( proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections ) { assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); } else { - assertEq(proposalVotingModule, uint8(i)); + // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID + assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); } } } @@ -1541,14 +2258,19 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal vm.prank(topDelegate_A); bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); assertEq(proposalHash, expectedHash); // Verify proposal data was stored correctly - (address proposer, ProposalValidator.ProposalType proposalType, bool inVoting, uint256 approvalCount) = - validator.getProposalData(proposalHash); + ( + address proposer, + ProposalValidator.ProposalType proposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); assertEq( @@ -1556,8 +2278,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal uint8(ProposalValidator.ProposalType.CouncilMemberElections), "Proposal type should be CouncilMemberElections" ); - assertFalse(inVoting, "Proposal should not be in voting yet"); + assertFalse(movedToVote, "Proposal should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1593,7 +2316,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER ); } @@ -1604,7 +2327,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1613,7 +2336,9 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal(criteriaValue, emptyOptions, proposalDescription, attestationUid); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); } function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { @@ -1635,7 +2360,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Submit first proposal vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); // Create new attestation for second attempt @@ -1649,7 +2374,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_B); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, secondAttestation + criteriaValue, optionDescriptions, proposalDescription, secondAttestation, CYCLE_NUMBER ); } @@ -1673,7 +2398,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1701,7 +2426,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, invalidAttestation + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER ); } @@ -1716,7 +2441,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } @@ -1737,7 +2462,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER ); } } @@ -1808,8 +2533,9 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init emit ProposalMovedToVote(expectedHash, proposer); vm.prank(proposer); - bytes32 proposalHash = - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1817,14 +2543,16 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertTrue(inVoting, "MaintenanceUpgrade should be in voting immediately"); + assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( @@ -1867,8 +2595,9 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init emit ProposalVotingModuleData(expectedHash, votingModuleData); vm.prank(proposer); - bytes32 proposalHash = - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); assertEq(proposalHash, expectedHash); @@ -1876,14 +2605,16 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init ( address storedProposer, ProposalValidator.ProposalType storedProposalType, - bool inVoting, - uint256 approvalCount + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle ) = validator.getProposalData(proposalHash); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(inVoting, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } @@ -1910,7 +2641,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { @@ -1922,7 +2655,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { @@ -1935,7 +2670,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I // Try to submit with different address than attested vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { @@ -1945,7 +2682,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { @@ -1958,7 +2695,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(excessiveThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { @@ -1998,7 +2737,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); // Create new attestation for second attempt bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); @@ -2009,7 +2750,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.prank(topDelegate_B); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, secondAttestation, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, secondAttestation, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { @@ -2038,7 +2781,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { @@ -2065,7 +2810,9 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, invalidAttestation, proposalType); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER + ); } function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { @@ -2089,6 +2836,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType); + validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } } From a96293c345873fdd4f980e5005096634d4f99cd7 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:01:57 -0300 Subject: [PATCH 52/73] refactor: improve tests (#440) * refactor: order tests based on implementation * chore: remove unused variables * test: add fuzzing for setter tests * test: add fuzzing to hashProposalWithModule tests * test: improve funding proposal tests * test: improve council member election tests * fix: pre-pr --- .../src/governance/ProposalValidator.sol | 5 +- .../test/governance/ProposalValidator.t.sol | 3060 ++++++++--------- 2 files changed, 1522 insertions(+), 1543 deletions(-) diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index bd8167e57a0..a9559e32ba6 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -640,10 +640,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), - proposalVotingModuleData, - _proposalDescription, - uint8(proposalType) + VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7721ac54f9e..fd369cf994c 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -133,13 +133,7 @@ contract ProposalValidator_Init is CommonTest { address owner; address user; address topDelegate_A = makeAddr("topDelegate_A"); - address topDelegate_B = makeAddr("topDelegate_B"); - address topDelegate_C = makeAddr("topDelegate_C"); - address topDelegate_D = makeAddr("topDelegate_D"); bytes32 topDelegateAttestation_A; - bytes32 topDelegateAttestation_B; - bytes32 topDelegateAttestation_C; - bytes32 topDelegateAttestation_D; address approvedProposer = makeAddr("approvedProposer"); address approvalVotingModule; address optimisticVotingModule; @@ -150,7 +144,6 @@ contract ProposalValidator_Init is CommonTest { IProposalTypesConfigurator public proposalTypesConfigurator; bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; - bytes32 public proposalHash; event ProposalSubmitted( bytes32 indexed proposalHash, @@ -225,12 +218,6 @@ contract ProposalValidator_Init is CommonTest { ); } - /// @notice Helper function to set both funding proposal types. - function _setFundingProposalTypes() internal { - _setGovernanceFundProposalType(); - _setCouncilBudgetProposalType(); - } - /// @notice Helper function to set ProtocolOrGovernorUpgrade proposal type data. function _setProtocolOrGovernorUpgradeProposalType() internal { _setProposalTypeData( @@ -253,11 +240,6 @@ contract ProposalValidator_Init is CommonTest { ); } - /// @notice Helper function to set both upgrade proposal types. - function _setUpgradeProposalTypes() internal { - _setProtocolOrGovernorUpgradeProposalType(); - _setMaintenanceUpgradeProposalType(); - } /// @notice Helper to create minimal valid arrays for funding proposal error tests function _createMinimalFundingArrays() @@ -427,7 +409,14 @@ contract ProposalValidator_Init is CommonTest { ); // 1 vote as default for being able to move to vote - validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, + proposer, + ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, + false, + PROPOSAL_REQUIRED_APPROVALS, + CYCLE_NUMBER + ); } /// @notice Helper function to create a proposal for move to vote for council elections @@ -445,7 +434,14 @@ contract ProposalValidator_Init is CommonTest { approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData(proposalHash_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, + proposer, + ProposalValidator.ProposalType.CouncilMemberElections, + false, + PROPOSAL_REQUIRED_APPROVALS, + CYCLE_NUMBER + ); } /// @notice Helper function to create a proposal for move to vote for a funding proposal type @@ -467,7 +463,9 @@ contract ProposalValidator_Init is CommonTest { approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData(proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER + ); } /// @notice Helper function to setup proposal types configurator mocks @@ -553,9 +551,6 @@ contract ProposalValidator_Init is CommonTest { // Create attestations for top delegates topDelegateAttestation_A = _createTopDelegateAttestation(topDelegate_A); - topDelegateAttestation_B = _createTopDelegateAttestation(topDelegate_B); - topDelegateAttestation_C = _createTopDelegateAttestation(topDelegate_C); - topDelegateAttestation_D = _createTopDelegateAttestation(topDelegate_D); } /// @notice Helper to create a valid attestation for an approved proposer @@ -601,1171 +596,802 @@ contract ProposalValidator_Init is CommonTest { } } -/// @title ProposalValidator_ApproveProposal_Test -/// @notice Happy path tests for approveProposal function -contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { - function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Ensure the proposal hash is not 0 - vm.assume(_proposalHash != bytes32(0)); +/// @title ProposalValidator_Version_Test +/// @notice Tests for the version function +contract ProposalValidator_Version_Test is ProposalValidator_Init { + function test_version_succeeds() public { + string memory versionString = validator.version(); + assertEq(versionString, "1.0.0-beta.1"); + } +} - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_Initialize_Test +/// @notice Tests for the initialize function +contract ProposalValidator_Initialize_Test is ProposalValidator_Init { + /// @dev Override to create validator proxy without initialization for testing + function _initializeValidator() internal override { + // Create mock addresses + proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + impl = new ProposalValidatorForTest( + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + ); + validator = ProposalValidatorForTest(address(new Proxy(owner))); + } - // Expect event to be emitted when approving - vm.expectEmit(address(validator)); - emit ProposalApproved(_proposalHash, topDelegate_A); + function test_initialize_succeeds() public { + ( + ProposalValidator.ProposalType[] memory proposalTypes, + ProposalValidator.ProposalTypeData[] memory proposalTypesData + ) = _getProposalTypesAndData(); - // Approve the proposal, use the attestation of the top delegate that was created in setUp - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + vm.prank(owner); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + proposalTypesConfigurator, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); - // Check that the proposal data has been updated - assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); + // Verify initialization was successful + assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.owner(), owner); - (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); - assertEq(approvalCount, 1); + // Verify voting cycle data + (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + validator.votingCycles(CYCLE_NUMBER); + assertEq(startBlock, START_BLOCK); + assertEq(duration, DURATION); + assertEq(distributionLimit, DISTRIBUTION_LIMIT); + assertEq(movedToVoteTokenCount, 0); + + // Verify proposal type data + for (uint256 i = 0; i < proposalTypes.length; i++) { + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); + if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { + assertEq(requiredApprovals, 0); + } else { + assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); + } + + // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID + if ( + proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget + || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections + ) { + assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); + } else { + // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID + assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); + } + } + } + + function test_initialize_mismatchedArrayLengths_reverts() public { + ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); + proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; + proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + + // Create mismatched array with different length + ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); + proposalTypesData[0] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 0 + }); + proposalTypesData[1] = ProposalValidator.ProposalTypeData({ + requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, + proposalVotingModule: 1 + }); + + vm.prank(owner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + IProxy(payable(address(validator))).upgradeToAndCall( + address(impl), + abi.encodeCall( + impl.initialize, + ( + owner, + proposalTypesConfigurator, + CYCLE_NUMBER, + START_BLOCK, + DURATION, + DISTRIBUTION_LIMIT, + DISTRIBUTION_THRESHOLD, + proposalTypes, + proposalTypesData + ) + ) + ); } } -/// @title ProposalValidator_ApproveProposal_TestFail -/// @notice Sad path tests for approveProposal function -contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { +/// @title ProposalValidator_SubmitUpgradeProposal_Test +/// @notice Happy path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { + string proposalDescription; + function setUp() public override { super.setUp(); - } - function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { - // There is no stored proposal data so this will revert - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + + proposalDescription = "Protocol Upgrade Proposal"; } - function test_approveProposal_proposalAlreadyApproved_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue + function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( + uint248 againstThreshold, + address proposer ) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); - // Mock the proposal as already approved by the top delegate - validator.mockApproveProposal(_proposalHash, topDelegate_A); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; - function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); - // create a new schema - vm.prank(topDelegate_A); - bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "string top100, string date", ISchemaResolver(address(0)), true - ); + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // create an attestation with the new schema - vm.prank(topDelegate_A); - bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: _invalidSchemaUid, - data: AttestationRequestData({ - recipient: topDelegate_A, - expirationTime: 0, - revocable: true, - refUID: bytes32(0), - data: abi.encode("top100", false, "2000-01-01"), - value: 0 - }) - }) + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _invalidAttestationUid); - } + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); - function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) - }) + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) ); - vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } - - function test_approveProposal_invalidAttestationCaller_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue, - address _caller - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Ensure the caller is not a top delegate - vm.assume( - _caller != topDelegate_A && _caller != topDelegate_B && _caller != topDelegate_C && _caller != topDelegate_D - ); + // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + vm.expectEmit(address(validator)); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(_caller); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); - } + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, proposer); - function test_approveProposal_invalidAttestationPartialDelegation_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + vm.prank(proposer); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + assertEq(proposalHash, expectedHash); - // create an attestation with partial delegation - vm.prank(owner); - bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: topDelegate_A, - expirationTime: 0, - revocable: true, - refUID: bytes32(0), - data: abi.encode("top100", true, "2000-01-01"), - value: 0 - }) - }) - ); + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } - function test_approveProposal_nonExistentAttestation_reverts( - bytes32 _proposalHash, - uint8 proposalTypeValue, - bytes32 _nonExistentAttestationUid + function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( + uint248 againstThreshold, + address proposer ) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Ensure the attestation uid is not one of the valid ones - vm.assume( - _nonExistentAttestationUid != topDelegateAttestation_A - && _nonExistentAttestationUid != topDelegateAttestation_B - && _nonExistentAttestationUid != topDelegateAttestation_C - && _nonExistentAttestationUid != topDelegateAttestation_D - ); - - // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); - // Expect the invalid attestation error to be reverted when attestation doesn't exist - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _nonExistentAttestationUid); - } -} + // Bound againstThreshold to valid range (1 to 10000 basis points) + againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes votingModuleData; - bytes32 expectedHash; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - function setUp() public override { - super.setUp(); + // Create attestation for the proposal + bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( - approvedProposer, againstThreshold, proposalDescription + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); - // Move to vote - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); - assertTrue(movedToVote, "Proposal should be in voting"); + vm.prank(proposer); + bytes32 proposalHash = validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + + assertEq(proposalHash, expectedHash); + + // Verify proposal data was stored correctly + ( + address storedProposer, + ProposalValidator.ProposalType storedProposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); + + assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); + assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes votingModuleData; - bytes32 expectedHash; +/// @title ProposalValidator_SubmitUpgradeProposal_TestFail +/// @notice Sad path tests for submitUpgradeProposal function +contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { + string proposalDescription; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = _createUpgradeProposalForMoveToVote( - approvedProposer, againstThreshold, proposalDescription - ); + _setProtocolOrGovernorUpgradeProposalType(); + _setMaintenanceUpgradeProposalType(); + + proposalDescription = "Test upgrade proposal"; } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) - public - { - vm.assume(_caller != approvedProposer); + function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) + proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; // 50% + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts( - uint248 _againstThreshold - ) - public - { - // This will generate a different proposal hash which will make the proposal type wrong - vm.assume(_againstThreshold != againstThreshold); + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - - // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { + uint248 zeroThreshold = 0; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { + // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR + excessiveThreshold = + uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } -} -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; - uint128 criteriaValue = 1; - bytes32 expectedHash; - bytes votingModuleData; - string proposalDescription = "Test proposal"; - string[] optionsDescriptions = new string[](2); + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function setUp() public override { - super.setUp(); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Create a proposal for move to vote with 1 top choice and 2 options - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - proposalDescription + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + // For MaintenanceUpgrade, mock the governor.proposeWithModule call + if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + } - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); - assertTrue(movedToVote, "Proposal should be in voting"); - } -} + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; - uint128 criteriaValue = 1; - string proposalDescription = "Test proposal"; - string[] optionsDescriptions = new string[](2); - bytes32 expectedHash; - bytes votingModuleData; + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - function setUp() public override { - super.setUp(); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - // Create a proposal for move to vote with 1 top choice and 2 options - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - proposalDescription + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); } - function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) - public - { - vm.assume(_caller != approvedProposer); + function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); - } + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { - // This will generate a different proposal hash which will make the proposal type wrong - uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, proposalType), + value: 0 + }) + }) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER + ); } - function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { - // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { + // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + uint248 againstThreshold = 5000; - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); - } + // Create valid attestation first (make it revocable) + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: attestationUid, value: 0 }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(block.number + DURATION + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); } +} - function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test +/// @notice Happy path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + string proposalDescription; - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + function setUp() public override { + super.setUp(); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) - ); + _setCouncilMemberElectionsProposalType(); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + proposalDescription = "Council Member Elections Q4 2024"; } -} -contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { - ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; - ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; - uint128 criteriaValue = 1; - string governanceFundProposalDescription = "Test governance fund proposal"; - string councilBudgetProposalDescription = "Test council budget proposal"; - string[] optionsDescriptions = new string[](2); - address[] optionsRecipients = new address[](2); - uint256[] optionsAmounts = new uint256[](2); - bytes32 expectedGovernanceFundHash; - bytes32 expectedCouncilBudgetHash; - bytes governanceFundVotingModuleData; - bytes councilBudgetVotingModuleData; - - function setUp() public override { - super.setUp(); - - // Create option descriptions for the proposals - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; + function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { + optionCount = uint8(bound(optionCount, 2, 5)); // Minimum 2 options to have valid criteria < optionCount + criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount - // Create option recipients for the proposals - optionsRecipients[0] = makeAddr("optionRecipient1"); - optionsRecipients[1] = makeAddr("optionRecipient2"); + // Create dynamic array of option descriptions based on option count + string[] memory optionDescriptions = new string[](optionCount); + for (uint256 i = 0; i < optionCount; i++) { + optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); + } - // Create option amounts for the proposals - optionsAmounts[0] = 100 ether; - optionsAmounts[1] = 200 ether; + // Create attestation for the proposal + bytes32 attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Create one proposal for each type - (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType - ); - (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - } - - function test_moveToVoteFundingProposal_governanceFund_succeeds() public { - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call + // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - ( - VotingModule(approvalVotingModule), - governanceFundVotingModuleData, - governanceFundProposalDescription, - uint8(governanceFundProposalType) - ) - ), - abi.encode(uint256(expectedGovernanceFundHash)) + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) ); - // Expect the ProposalMovedToVote event to be emitted + // Expect ProposalSubmitted event vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); - - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType + emit ProposalSubmitted( + expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections ); - // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); - assertTrue(movedToVote, "Proposal should be in voting"); - } + // Expect ProposalVotingModuleData event + vm.expectEmit(address(validator)); + emit ProposalVotingModuleData(expectedHash, votingModuleData); - function test_moveToVoteFundingProposal_councilBudget_succeeds() public { - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - ( - VotingModule(approvalVotingModule), - councilBudgetVotingModuleData, - councilBudgetProposalDescription, - uint8(councilBudgetProposalType) - ) - ), - abi.encode(uint256(expectedCouncilBudgetHash)) + vm.prank(topDelegate_A); + bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - // Expect the ProposalMovedToVote event to be emitted - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + assertEq(proposalHash, expectedHash); - // Move to vote - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType + // Verify proposal data was stored correctly + ( + address proposer, + ProposalValidator.ProposalType proposalType, + bool movedToVote, + uint256 approvalCount, + uint256 votingCycle + ) = validator.getProposalData(proposalHash); + + assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); + assertEq( + uint8(proposalType), + uint8(ProposalValidator.ProposalType.CouncilMemberElections), + "Proposal type should be CouncilMemberElections" ); + assertFalse(movedToVote, "Proposal should not be in voting yet"); + assertEq(approvalCount, 0, "Approval count should be 0"); + assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } } -contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; - ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; - uint128 criteriaValue = 1; - string governanceFundProposalDescription = "Test governance fund proposal"; - string councilBudgetProposalDescription = "Test council budget proposal"; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - bytes32 governanceFundExpectedHash; - bytes32 councilBudgetExpectedHash; - bytes governanceFundVotingModuleData; - bytes councilBudgetVotingModuleData; +/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail +/// @notice Sad path tests for submitCouncilMemberElectionsProposal function +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + uint128 criteriaValue; + string[] optionDescriptions; + string proposalDescription; + bytes32 attestationUid; function setUp() public override { super.setUp(); - (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); - (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType - ); - (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType - ); + _setCouncilMemberElectionsProposalType(); + + criteriaValue = 2; + optionDescriptions = new string[](3); + optionDescriptions[0] = "Candidate A"; + optionDescriptions[1] = "Candidate B"; + optionDescriptions[2] = "Candidate C"; + + proposalDescription = "Test Council Elections"; + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } - function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( - uint8 _proposalTypeValue, - string memory _proposalDescription - ) + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidProposal_reverts( - uint8 _proposalTypeValue, - uint128 _criteriaValue - ) - public - { - // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will - // not find the proposal - vm.assume(_criteriaValue != criteriaValue); - - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { + vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } + // Try to submit with different address than attested + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(fuzzedProposer); // Different from attested topDelegate_A + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { + string[] memory emptyOptions = new string[](0); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( - uint8 _wrongProposalTypeValue, - uint8 _validProposalTypeValue - ) - public - { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); - ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); - - _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); - ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); + function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - string memory proposalDescription; - if (validProposalType == governanceFundProposalType) { - // Set proposal data proposal type to a different value - validator.setProposalData(governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data proposal type to a different value - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Mock proposalSnapshot to return 0 for first submission + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - proposalDescription, - validProposalType + // Submit first proposal + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - } - - function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Attempt to submit identical proposal should revert + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { + // Calculate expected proposal hash + bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes32 expectedHash = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - // Set proposal data movedToVote to true - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; - } else { - // Set proposal data movedToVote to true - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; - } + // Mock proposalSnapshot to return non-zero (proposal already exists in governor) + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(1000) // Non-zero indicates proposal exists + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); - // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } + function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) + public + { + vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Create attestation but don't use proper owner as attester + vm.prank(fuzzedAttester); // Not the owner + bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: address(0), + expirationTime: 0, + revocable: false, + refUID: bytes32(0), + data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + value: 0 + }) + }) + ); - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(START_BLOCK + DURATION + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( - uint8 _proposalTypeValue + function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( + uint128 invalidCriteriaValue ) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } - - // Set the first option amount to exceed the distribution threshold - optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + // Bound invalidCriteriaValue to be greater than options length + invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); - vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } - function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; - } else { - proposalDescription = councilBudgetProposalDescription; - } - - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - - string[] memory _optionsDescriptions = new string[](3); - address[] memory _optionsRecipients = new address[](3); - uint256[] memory _optionsAmounts = new uint256[](3); + function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { + // Create valid attestation first (make it revocable) + bytes32 revocableAttestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - _optionsDescriptions[0] = "Option 1"; - _optionsDescriptions[1] = "Option 2"; - _optionsDescriptions[2] = "Option 3"; + // Revoke the attestation + vm.prank(owner); + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) + }) + ); - _optionsRecipients[0] = makeAddr("optionRecipient1"); - _optionsRecipients[1] = makeAddr("optionRecipient2"); - _optionsRecipients[2] = makeAddr("optionRecipient3"); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER + ); + } +} - _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; +/// @title ProposalValidator_SubmitFundingProposal_Test +/// @notice Happy path tests for submitFundingProposal function +contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { + uint128 criteriaValue = 1000 ether; + string description = "Test funding proposal"; - _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - _optionsDescriptions, - _optionsRecipients, - _optionsAmounts, - proposalDescription, - proposalType - ); + function setUp() public override { + super.setUp(); - vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); - vm.prank(approvedProposer); - vm.roll(START_BLOCK + 1); - validator.moveToVoteFundingProposal( - criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType - ); + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); } - function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( - uint8 _proposalTypeValue, - bytes32 _randomHash + function testFuzz_submitFundingProposal_succeeds( + uint8 proposalTypeValue, + uint8 optionCount, + uint256 amount, + address proposer ) public { - vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + // Assume proposer is not zero address + vm.assume(proposer != address(0)); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - - // Mock the proposal types configurator call - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - - bytes memory votingModuleData; - string memory proposalDescription; - if (proposalType == governanceFundProposalType) { - votingModuleData = governanceFundVotingModuleData; - proposalDescription = governanceFundProposalDescription; - } else { - votingModuleData = councilBudgetVotingModuleData; - proposalDescription = councilBudgetProposalDescription; - } - - // Mock the governor.proposeWithModule call - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(_randomHash)) - ); - - vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); - vm.prank(approvedProposer); - validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType - ); - } -} - -/// @title ProposalValidator_CanApproveProposal_Test -/// @notice Tests for the canApproveProposal function -contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public { - // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); - assertTrue(canApprove); - } - - function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { - // Ensure the attestation uid is not one of the top delegates - vm.assume( - attestationUid != topDelegateAttestation_A && attestationUid != topDelegateAttestation_B - && attestationUid != topDelegateAttestation_C && attestationUid != topDelegateAttestation_D - ); - - bool canApprove; - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { - canApprove = result_; - } catch { - canApprove = false; - } - - assertEq(canApprove, false); - } -} - -/// @title ProposalValidator_Version_Test -/// @notice Tests for the version function -contract ProposalValidator_Version_Test is ProposalValidator_Init { - function test_version_succeeds() public { - string memory versionString = validator.version(); - assertEq(versionString, "1.0.0-beta.1"); - } -} - -/// @title ProposalValidator_Setters_Test -/// @notice Tests for setter functions -contract ProposalValidator_Setters_Test is ProposalValidator_Init { - function testFuzz_setVotingCycleData_succeeds( - uint256 cycleNumber, - uint256 startBlock, - uint256 duration, - uint256 distributionLimit - ) - public - { - vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle - - // Expect the VotingCycleDataSet event to be emitted - vm.expectEmit(address(validator)); - emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); - - vm.prank(owner); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - - ( - uint256 actualStartBlock, - uint256 actualDuration, - uint256 actualDistributionLimit, - uint256 actualMovedToVoteTokenCount - ) = validator.votingCycles(cycleNumber); - - assertEq(actualStartBlock, startBlock); - assertEq(actualDuration, duration); - assertEq(actualDistributionLimit, distributionLimit); - assertEq(actualMovedToVoteTokenCount, 0); - } - - function test_setVotingCycleData_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - } - - function test_setVotingCycleData_votingCycleAlreadySet_reverts() public { - vm.prank(owner); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - - vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); - vm.prank(owner); - validator.setVotingCycleData(2, block.number, 100, 10000 ether); - } - - function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { - // Expect the DistributionThresholdSet event to be emitted - vm.expectEmit(address(validator)); - emit DistributionThresholdSet(newDistributionThreshold); - - vm.prank(owner); - validator.setDistributionThreshold(newDistributionThreshold); - - assertEq(validator.distributionThreshold(), newDistributionThreshold); - } - - function test_setDistributionThreshold_notOwner_reverts() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setDistributionThreshold(10000 ether); - } - - function testFuzz_setProposalTypeData_succeeds( - uint8 proposalTypeValue, - uint256 newRequiredApprovals, - uint8 newProposalTypeId - ) - public - { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ - requiredApprovals: newRequiredApprovals, - proposalVotingModule: newProposalTypeId - }); - - // Expect the ProposalTypeDataSet event to be emitted - vm.expectEmit(address(validator)); - emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); - - vm.prank(owner); - validator.setProposalTypeData(proposalType, newData); - - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); - assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newProposalTypeId); - } - - function test_setProposalTypeData_notOwner_reverts() public { - ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); - } -} - -/// @title ProposalValidator_HashProposalWithModule_Test -/// @notice Tests for the hashProposalWithModule function -contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { - function test_hashProposalWithModule_succeeds() public { - address testModule = makeAddr("testModule"); - bytes memory testProposalData = abi.encode("test", "proposal", "data"); - bytes32 testDescriptionHash = keccak256("test description"); - - bytes32 hash = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - assertTrue(hash != bytes32(0)); - } - - function test_hashProposalWithModule_consistentHash_succeeds() public { - address testModule = makeAddr("testModule"); - bytes memory testProposalData = abi.encode("test data"); - bytes32 testDescriptionHash = keccak256("description"); - - bytes32 hash1 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - bytes32 hash2 = validator.hashProposalWithModule(testModule, testProposalData, testDescriptionHash); - - assertEq(hash1, hash2); - } - - function test_hashProposalWithModule_differentInputs_succeeds() public { - address module1 = makeAddr("module1"); - address module2 = makeAddr("module2"); - bytes memory data = abi.encode("data"); - bytes32 descHash = keccak256("desc"); - - bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); - bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); - - assertTrue(hash1 != hash2); - } -} - -/// @title ProposalValidator_SubmitFundingProposal_Test -/// @notice Happy path tests for submitFundingProposal function -contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - string description; - - function setUp() public override { - super.setUp(); - - _setFundingProposalTypes(); - - criteriaValue = 1000 ether; - } - - function testFuzz_submitFundingProposal_succeeds( - uint8 proposalTypeValue, - uint8 optionCount, - uint256 amount, - address proposer - ) - public - { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - - // Bound option count between 1 and 50 for reasonable test execution - optionCount = uint8(bound(optionCount, 1, 50)); + // Bound option count between 1 and 5 for reasonable test execution + optionCount = uint8(bound(optionCount, 1, 5)); // Bound amount from 0 to DISTRIBUTION_THRESHOLD (inclusive) amount = bound(amount, 0, DISTRIBUTION_THRESHOLD); - // Create arrays based on option count - string[] memory descriptions = new string[](optionCount); - address[] memory recipients = new address[](optionCount); - uint256[] memory amounts = new uint256[](optionCount); + // Start with minimal arrays and extend based on option count + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(); for (uint256 i = 0; i < optionCount; i++) { - descriptions[i] = string(abi.encodePacked("Option ", vm.toString(i))); - recipients[i] = makeAddr(string(abi.encodePacked("recipient", vm.toString(i)))); - amounts[i] = amount; // Use the same bounded amount for all options + descriptions[i] = descriptions[0]; + recipients[i] = recipients[0]; + amounts[i] = amount; } // Calculate expected proposal hash @@ -1824,7 +1450,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I function setUp() public override { super.setUp(); // Set both funding proposal types to use the approval voting module - _setFundingProposalTypes(); + _setGovernanceFundProposalType(); + _setCouncilBudgetProposalType(); } function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { @@ -2090,752 +1717,1107 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I } } -/// @title ProposalValidator_Initialize_Test -/// @notice Tests for the initialize function -contract ProposalValidator_Initialize_Test is ProposalValidator_Init { - /// @dev Override to create validator proxy without initialization for testing - function _initializeValidator() internal override { - // Create mock addresses - proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); +/// @title ProposalValidator_ApproveProposal_Test +/// @notice Happy path tests for approveProposal function +contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Ensure the proposal hash is not 0 + vm.assume(_proposalHash != bytes32(0)); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken - ); - validator = ProposalValidatorForTest(address(new Proxy(owner))); - } + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function test_initialize_succeeds() public { - ( - ProposalValidator.ProposalType[] memory proposalTypes, - ProposalValidator.ProposalTypeData[] memory proposalTypesData - ) = _getProposalTypesAndData(); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - vm.prank(owner); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - proposalTypesConfigurator, - CYCLE_NUMBER, - START_BLOCK, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) - ); + // Expect event to be emitted when approving + vm.expectEmit(address(validator)); + emit ProposalApproved(_proposalHash, topDelegate_A); - // Verify initialization was successful - assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); - assertEq(validator.owner(), owner); + // Approve the proposal, use the attestation of the top delegate that was created in setUp + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); - // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = - validator.votingCycles(CYCLE_NUMBER); - assertEq(startBlock, START_BLOCK); - assertEq(duration, DURATION); - assertEq(distributionLimit, DISTRIBUTION_LIMIT); - assertEq(movedToVoteTokenCount, 0); + // Check that the proposal data has been updated + assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); - // Verify proposal type data - for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); - if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { - assertEq(requiredApprovals, 0); - } else { - assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - } + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); + assertEq(approvalCount, 1); + } +} - // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID - if ( - proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections - ) { - assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); - } else { - // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID - assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); - } - } +/// @title ProposalValidator_ApproveProposal_TestFail +/// @notice Sad path tests for approveProposal function +contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { + function setUp() public override { + super.setUp(); } - function test_initialize_mismatchedArrayLengths_reverts() public { - ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); - proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; - proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; + function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + // There is no stored proposal data so this will revert + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } - // Create mismatched array with different length - ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); - proposalTypesData[0] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 - }); - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 - }); + function test_approveProposal_proposalAlreadyApproved_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + // Mock the proposal as already approved by the top delegate + validator.mockApproveProposal(_proposalHash, topDelegate_A); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // create a new schema + vm.prank(topDelegate_A); + bytes32 _invalidSchemaUid = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( + "string top100, string date", ISchemaResolver(address(0)), true + ); + + // create an attestation with the new schema + vm.prank(topDelegate_A); + bytes32 _invalidAttestationUid = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: _invalidSchemaUid, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", false, "2000-01-01"), + value: 0 + }) + }) + ); + + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _invalidAttestationUid); + } + function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + // revoke the attestation vm.prank(owner); - vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - proposalTypesConfigurator, - CYCLE_NUMBER, - START_BLOCK, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) + IEAS(Predeploys.EAS).revoke( + RevocationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: RevocationRequestData({ uid: topDelegateAttestation_A, value: 0 }) + }) ); + + vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } -} -/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test -/// @notice Happy path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { - string proposalDescription; + function test_approveProposal_invalidAttestationCaller_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + address _caller + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - function setUp() public override { - super.setUp(); + // Ensure the caller is not a top delegate + vm.assume(_caller != topDelegate_A); - _setCouncilMemberElectionsProposalType(); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = "Council Member Elections Q4 2024"; + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(_caller); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); } - function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { - optionCount = uint8(bound(optionCount, 2, type(uint8).max)); // Minimum 2 options to have valid criteria < - // optionCount - criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount - - // Create dynamic array of option descriptions based on option count - string[] memory optionDescriptions = new string[](optionCount); - for (uint256 i = 0; i < optionCount; i++) { - optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); - } + function test_approveProposal_invalidAttestationPartialDelegation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Create attestation for the proposal - bytes32 attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + // create an attestation with partial delegation + vm.prank(owner); + bytes32 _attestationUidWithPartialDelegation = IEAS(Predeploys.EAS).attest( + AttestationRequest({ + schema: TOP_DELEGATES_ATTESTATION_SCHEMA_UID, + data: AttestationRequestData({ + recipient: topDelegate_A, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("top100", true, "2000-01-01"), + value: 0 + }) + }) ); - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) - ); + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + } - // Expect ProposalSubmitted event - vm.expectEmit(address(validator)); - emit ProposalSubmitted( - expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections - ); + function test_approveProposal_nonExistentAttestation_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + bytes32 _nonExistentAttestationUid + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Expect ProposalVotingModuleData event - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Ensure the attestation uid is not one of the valid ones + vm.assume(_nonExistentAttestationUid != topDelegateAttestation_A); - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + // Set mock proposal data of a random proposal in the validator contract + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // Expect the invalid attestation error to be reverted when attestation doesn't exist + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + } +} - assertEq(proposalHash, expectedHash); +/// @title ProposalValidator_CanApproveProposal_Test +/// @notice Tests for the canApproveProposal function +contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { + function test_canApproveProposal_returnTrue_succeeds() public { + // Attestation already created in setUp + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + assertTrue(canApprove); + } - // Verify proposal data was stored correctly - ( - address proposer, - ProposalValidator.ProposalType proposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { + // Ensure the attestation uid is not one of the top delegates + vm.assume(attestationUid != topDelegateAttestation_A); - assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); - assertEq( - uint8(proposalType), - uint8(ProposalValidator.ProposalType.CouncilMemberElections), - "Proposal type should be CouncilMemberElections" - ); - assertFalse(movedToVote, "Proposal should not be in voting yet"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + bool canApprove; + // Expect the invalid attestation error to be reverted + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); + try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { + canApprove = result_; + } catch { + canApprove = false; + } + + assertEq(canApprove, false); } } -/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail -/// @notice Sad path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionDescriptions; - string proposalDescription; - bytes32 attestationUid; +/// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test +/// @notice Happy path tests for moveToVoteProtocolOrGovernorUpgradeProposal function +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; + + function setUp() public override { + super.setUp(); + + (expectedHash, votingModuleData) = + _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); + } + + function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) + ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } +} + +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { + uint248 againstThreshold = 5000; // 50% + string proposalDescription = "Test proposal"; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes votingModuleData; + bytes32 expectedHash; function setUp() public override { super.setUp(); - _setCouncilMemberElectionsProposalType(); + (expectedHash, votingModuleData) = + _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); + } - criteriaValue = 2; - optionDescriptions = new string[](3); - optionDescriptions[0] = "Candidate A"; - optionDescriptions[1] = "Candidate B"; - optionDescriptions[2] = "Candidate C"; + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) public { + vm.assume(_caller != approvedProposer); - proposalDescription = "Test Council Elections"; - attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 _againstThreshold) public { - vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation + // This will generate a different proposal hash which will make the proposal type wrong + vm.assume(_againstThreshold != againstThreshold); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_unattestedProposer_reverts(address fuzzedProposer) public { - vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - // Try to submit with different address than attested - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { - string[] memory emptyOptions = new string[](0); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); - // Mock proposalSnapshot to return 0 for first submission + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) ); - _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(approvedProposer); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + } +} - // Submit first proposal - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + bytes32 expectedHash; + bytes votingModuleData; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); - // Create new attestation for second attempt - bytes32 secondAttestation = - _createApprovedProposerAttestation(topDelegate_B, ProposalValidator.ProposalType.CouncilMemberElections); + function setUp() public override { + super.setUp(); - // Attempt to submit identical proposal should revert - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, criteriaValue, optionsDescriptions, proposalDescription + ); + } + function test_moveToVoteCouncilMemberElectionsProposal_succeeds() public { + // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(topDelegate_B); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, secondAttestation, CYCLE_NUMBER + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(expectedHash)) ); + + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + assertTrue(movedToVote, "Proposal should be in voting"); } +} - function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { - // Calculate expected proposal hash - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; + uint128 criteriaValue = 1; + string proposalDescription = "Test proposal"; + string[] optionsDescriptions = new string[](2); + bytes32 expectedHash; + bytes votingModuleData; - // Mock proposalSnapshot to return non-zero (proposal already exists in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(1000) // Non-zero indicates proposal exists + function setUp() public override { + super.setUp(); + + // Create a proposal for move to vote with 1 top choice and 2 options + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; + (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); + } - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) public { + vm.assume(_caller != approvedProposer); + // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); + vm.prank(_caller); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_attestationNotFromOwner_reverts(address fuzzedAttester) - public - { - vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { + // This will generate a different proposal hash which will make the proposal type wrong + uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp - // Create attestation but don't use proper owner as attester - vm.prank(fuzzedAttester); // Not the owner - bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, - revocable: false, - refUID: bytes32(0), - data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), - value: 0 - }) - }) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER - ); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); } - function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( - uint128 invalidCriteriaValue - ) - public - { - // Bound invalidCriteriaValue to be greater than options length - invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { + // Set proposal data movedToVote to true + validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + } + + function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(block.number + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function test_submitCouncilMemberElectionsProposal_attestationRevoked_reverts() public { - // Create valid attestation first (make it revocable) - bytes32 revocableAttestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { + vm.assume(_randomHash != expectedHash); - // Revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: revocableAttestationUid, value: 0 }) - }) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } } -/// @title ProposalValidator_SubmitUpgradeProposal_Test -/// @notice Happy path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { - string proposalDescription; +contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions = new string[](2); + address[] optionsRecipients = new address[](2); + uint256[] optionsAmounts = new uint256[](2); + bytes32 expectedGovernanceFundHash; + bytes32 expectedCouncilBudgetHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; function setUp() public override { super.setUp(); - _setUpgradeProposalTypes(); - - proposalDescription = "Protocol Upgrade Proposal"; - } - - function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( - uint248 againstThreshold, - address proposer - ) - public - { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + // Create option descriptions for the proposals + optionsDescriptions[0] = "Option 1"; + optionsDescriptions[1] = "Option 2"; - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Create option recipients for the proposals + optionsRecipients[0] = makeAddr("optionRecipient1"); + optionsRecipients[1] = makeAddr("optionRecipient2"); - // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + // Create option amounts for the proposals + optionsAmounts[0] = 100 ether; + optionsAmounts[1] = 200 ether; - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + // Create one proposal for each type + (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType ); + } - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) + function test_moveToVoteFundingProposal_governanceFund_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + ( + VotingModule(approvalVotingModule), + governanceFundVotingModuleData, + governanceFundProposalDescription, + uint8(governanceFundProposalType) + ) + ), + abi.encode(uint256(expectedGovernanceFundHash)) ); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + // Expect the ProposalMovedToVote event to be emitted + vm.expectEmit(address(validator)); + emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + + // Check that the proposal is in voting + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + assertTrue(movedToVote, "Proposal should be in voting"); + } + + function test_moveToVoteFundingProposal_councilBudget_succeeds() public { + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); // Mock the governor.proposeWithModule call _mockAndExpect( address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ( + VotingModule(approvalVotingModule), + councilBudgetVotingModuleData, + councilBudgetProposalDescription, + uint8(councilBudgetProposalType) + ) ), - abi.encode(uint256(expectedHash)) + abi.encode(uint256(expectedCouncilBudgetHash)) ); - // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote + // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Move to vote + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType + ); + } +} - vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, proposer); +contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { + ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; + ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; + uint128 criteriaValue = 1; + string governanceFundProposalDescription = "Test governance fund proposal"; + string councilBudgetProposalDescription = "Test council budget proposal"; + string[] optionsDescriptions; + address[] optionsRecipients; + uint256[] optionsAmounts; + bytes32 governanceFundExpectedHash; + bytes32 councilBudgetExpectedHash; + bytes governanceFundVotingModuleData; + bytes councilBudgetVotingModuleData; - vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + function setUp() public override { + super.setUp(); + + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + governanceFundProposalDescription, + governanceFundProposalType + ); + (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + councilBudgetProposalDescription, + councilBudgetProposalType ); + } - assertEq(proposalHash, expectedHash); + function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( + uint8 _proposalTypeValue, + string memory _proposalDescription + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // Verify proposal data was stored correctly - ( - address storedProposer, - ProposalValidator.ProposalType storedProposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + ); + } + + function test_moveToVoteFundingProposal_invalidProposal_reverts( + uint8 _proposalTypeValue, + uint128 _criteriaValue + ) + public + { + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // not find the proposal + vm.assume(_criteriaValue != criteriaValue); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - assertEq(storedProposer, proposer, "Proposer should match input"); - assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } - function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( - uint248 againstThreshold, - address proposer + function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( + uint8 _wrongProposalTypeValue, + uint8 _validProposalTypeValue ) public { - // Assume proposer is not zero address - vm.assume(proposer != address(0)); - - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); - // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + string memory proposalDescription; + if (validProposalType == governanceFundProposalType) { + // Set proposal data proposal type to a different value + validator.setProposalData( + governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data proposal type to a different value + validator.setProposalData( + councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = councilBudgetProposalDescription; + } - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + proposalDescription, + validProposalType ); + } - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events - vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData( + governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER + ); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data approved count to 0 since it is 1 by the approval on the setUp + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } - vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); + } - assertEq(proposalHash, expectedHash); + function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - // Verify proposal data was stored correctly - ( - address storedProposer, - ProposalValidator.ProposalType storedProposalType, - bool movedToVote, - uint256 approvalCount, - uint256 votingCycle - ) = validator.getProposalData(proposalHash); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + // Set proposal data movedToVote to true + validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = governanceFundProposalDescription; + } else { + // Set proposal data movedToVote to true + validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + proposalDescription = councilBudgetProposalDescription; + } - assertEq(storedProposer, proposer, "Proposer should match input"); - assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); - assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); - assertEq(approvalCount, 0, "Approval count should be 0"); - assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } -} -/// @title ProposalValidator_SubmitUpgradeProposal_TestFail -/// @notice Sad path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { - string proposalDescription; + function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - function setUp() public override { - super.setUp(); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - _setUpgradeProposalTypes(); + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - proposalDescription = "Test upgrade proposal"; + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.roll(START_BLOCK + DURATION + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); } - function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { - // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) - proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + uint8 _proposalTypeValue + ) + public + { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - uint248 againstThreshold = 5000; // 50% - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + // Set the first option amount to exceed the distribution threshold + optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); } - function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - vm.assume(fuzzedAttestationUid != validAttestationUid); // Ensure it's different from valid attestation + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER - ); - } + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { - vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer + string[] memory _optionsDescriptions = new string[](3); + address[] memory _optionsRecipients = new address[](3); + uint256[] memory _optionsAmounts = new uint256[](3); - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + _optionsDescriptions[0] = "Option 1"; + _optionsDescriptions[1] = "Option 2"; + _optionsDescriptions[2] = "Option 3"; - // Try to submit with different address than attested - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(fuzzedProposer); // Different from attested topDelegate_A - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + _optionsRecipients[0] = makeAddr("optionRecipient1"); + _optionsRecipients[1] = makeAddr("optionRecipient2"); + _optionsRecipients[2] = makeAddr("optionRecipient3"); + + _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; + + _createFundingProposalForMoveToVote( + approvedProposer, + criteriaValue, + _optionsDescriptions, + _optionsRecipients, + _optionsAmounts, + proposalDescription, + proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); + vm.prank(approvedProposer); + vm.roll(START_BLOCK + 1); + validator.moveToVoteFundingProposal( + criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType ); } - function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { - uint248 zeroThreshold = 0; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( + uint8 _proposalTypeValue, + bytes32 _randomHash + ) + public + { + vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); - } + // Mock the proposal types configurator call + _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { - // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR - excessiveThreshold = - uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); + bytes memory votingModuleData; + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + votingModuleData = governanceFundVotingModuleData; + proposalDescription = governanceFundProposalDescription; + } else { + votingModuleData = councilBudgetVotingModuleData; + proposalDescription = councilBudgetProposalDescription; + } - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + // Mock the governor.proposeWithModule call + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(uint256(_randomHash)) + ); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.roll(START_BLOCK + 1); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType ); } +} - function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_Setters_Test +/// @notice Tests for setter functions +contract ProposalValidator_Setters_Test is ProposalValidator_Init { + function testFuzz_setVotingCycleData_succeeds( + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle - uint248 againstThreshold = 5000; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + // Expect the VotingCycleDataSet event to be emitted + vm.expectEmit(address(validator)); + emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + vm.prank(owner); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); - // Mock proposalSnapshot to return 0 for first submission - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) - ); + ( + uint256 actualStartBlock, + uint256 actualDuration, + uint256 actualDistributionLimit, + uint256 actualMovedToVoteTokenCount + ) = validator.votingCycles(cycleNumber); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + assertEq(actualStartBlock, startBlock); + assertEq(actualDuration, duration); + assertEq(actualDistributionLimit, distributionLimit); + assertEq(actualMovedToVoteTokenCount, 0); + } - // For MaintenanceUpgrade, mock the governor.proposeWithModule call - if (proposalType == ProposalValidator.ProposalType.MaintenanceUpgrade) { - _mockAndExpect( - address(governor), - abi.encodeCall( - IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) - ), - abi.encode(uint256(expectedHash)) - ); - } + function testFuzz_setVotingCycleData_notOwner_reverts( + address caller, + uint256 cycleNumber, + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.assume(caller != owner); - // Submit first proposal - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER - ); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + } - // Create new attestation for second attempt - bytes32 secondAttestation = _createApprovedProposerAttestation(topDelegate_B, proposalType); + function testFuzz_setVotingCycleData_votingCycleAlreadySet_reverts( + uint256 startBlock, + uint256 duration, + uint256 distributionLimit + ) + public + { + vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER, startBlock, duration, distributionLimit); + } - // Attempt to submit identical proposal should revert - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { + // Expect the DistributionThresholdSet event to be emitted + vm.expectEmit(address(validator)); + emit DistributionThresholdSet(newDistributionThreshold); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.prank(owner); + validator.setDistributionThreshold(newDistributionThreshold); - vm.prank(topDelegate_B); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, secondAttestation, proposalType, CYCLE_NUMBER - ); + assertEq(validator.distributionThreshold(), newDistributionThreshold); } - function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + function testFuzz_setDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { + vm.assume(caller != owner); - uint248 againstThreshold = 5000; - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setDistributionThreshold(threshold); + } - // Calculate expected proposal hash - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) - ); + function testFuzz_setProposalTypeData_succeeds( + uint8 proposalTypeValue, + uint256 newRequiredApprovals, + uint8 newProposalTypeId + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // Mock proposalSnapshot to return non-zero (proposal already exists in governor) - _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(1000) // Non-zero indicates proposal exists - ); + ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ + requiredApprovals: newRequiredApprovals, + proposalVotingModule: newProposalTypeId + }); - vm.expectRevert(ProposalValidator.ProposalValidator_ProposalAlreadySubmitted.selector); + // Expect the ProposalTypeDataSet event to be emitted + vm.expectEmit(address(validator)); + emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); - _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + vm.prank(owner); + validator.setProposalTypeData(proposalType, newData); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER - ); + (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); + assertEq(requiredApprovals, newRequiredApprovals); + assertEq(proposalVotingModule, newProposalTypeId); } - function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { - vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - - uint248 againstThreshold = 5000; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + function testFuzz_setProposalTypeData_notOwner_reverts(address caller) public { + vm.assume(caller != owner); - // Create attestation but don't use proper owner as attester - vm.prank(fuzzedAttester); // Not the owner - bytes32 invalidAttestation = IEAS(Predeploys.EAS).attest( - AttestationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, - revocable: false, - refUID: bytes32(0), - data: abi.encode(topDelegate_A, proposalType), - value: 0 - }) - }) - ); + ProposalValidator.ProposalTypeData memory newData = + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); - vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal( - againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER - ); + vm.prank(caller); + vm.expectRevert("Ownable: caller is not the owner"); + validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } +} - function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { - // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); +/// @title ProposalValidator_HashProposalWithModule_Test +/// @notice Tests for the hashProposalWithModule function +contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { + function testFuzz_hashProposalWithModule_succeeds( + address module, + bytes memory proposalData, + bytes32 descriptionHash + ) + public + { + bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); + bytes32 expectedHash = + keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash)); - uint248 againstThreshold = 5000; + assertEq(hash, expectedHash); + } - // Create valid attestation first (make it revocable) - bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + function test_hashProposalWithModule_differentInputs_succeeds() public { + address module1 = makeAddr("module1"); + address module2 = makeAddr("module2"); + bytes memory data = abi.encode("data"); + bytes32 descHash = keccak256("desc"); - // Revoke the attestation - vm.prank(owner); - IEAS(Predeploys.EAS).revoke( - RevocationRequest({ - schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - data: RevocationRequestData({ uid: attestationUid, value: 0 }) - }) - ); + bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); + bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); - vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); - vm.prank(topDelegate_A); - validator.submitUpgradeProposal(againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); + assertTrue(hash1 != hash2); } } From dc8f01082493ecf7ec692632ddb0e29486d1a23d Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:48:36 +0300 Subject: [PATCH 53/73] fix: voting window to use timestamp (#442) * fix: voting window to use timestamp * fix: pre-pr * fix: improve test --- .../governance/IProposalValidator.sol | 10 +-- .../snapshots/abi/ProposalValidator.json | 18 +++- .../src/governance/ProposalValidator.sol | 51 ++++++----- .../test/governance/ProposalValidator.t.sol | 89 +++++++++---------- 4 files changed, 90 insertions(+), 78 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 2ba27a28d83..1693007ce3a 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -51,7 +51,7 @@ interface IProposalValidator is ISemver { event VotingCycleDataSet( uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); @@ -88,7 +88,7 @@ interface IProposalValidator is ISemver { } struct VotingCycleData { - uint256 startingBlock; + uint256 startingTimestamp; uint256 duration; uint256 votingCycleDistributionLimit; uint256 movedToVoteTokenCount; @@ -154,7 +154,7 @@ interface IProposalValidator is ISemver { function setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) external; @@ -170,7 +170,7 @@ interface IProposalValidator is ISemver { address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, @@ -203,7 +203,7 @@ interface IProposalValidator is ISemver { function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( - uint256 startingBlock, + uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit, uint256 movedToVoteTokenCount diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 4af4cf80adf..40ba0366dd0 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -177,7 +177,7 @@ }, { "internalType": "uint256", - "name": "_startBlock", + "name": "_startingTimestamp", "type": "uint256" }, { @@ -429,7 +429,7 @@ }, { "internalType": "uint256", - "name": "_startBlock", + "name": "_startingTimestamp", "type": "uint256" }, { @@ -613,7 +613,7 @@ "outputs": [ { "internalType": "uint256", - "name": "startingBlock", + "name": "startingTimestamp", "type": "uint256" }, { @@ -805,7 +805,7 @@ { "indexed": false, "internalType": "uint256", - "name": "startBlock", + "name": "startingTimestamp", "type": "uint256" }, { @@ -869,6 +869,16 @@ "name": "ProposalValidator_InvalidOptionsLength", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidProposal", + "type": "error" + }, + { + "inputs": [], + "name": "ProposalValidator_InvalidProposer", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidUpgradeProposalType", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index a9559e32ba6..1aa237ef4be 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -119,11 +119,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. - /// @param startBlock The block number of the starting block of the voting cycle. + /// @param startingTimestamp The starting timestamp of the voting cycle. /// @param duration The duration of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); /// @notice Emitted when the distribution threshold is set. @@ -171,12 +171,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Struct for storing voting cycle data. - /// @param startingBlock The block number of the starting block of the voting cycle. - /// @param duration The duration of the voting cycle. + /// @param startingTimestamp The starting timestamp of the voting cycle. + /// @param duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week 3 + /// of the voting cycle. /// @param votingCycleDistributionLimit The max amount of tokens that can be distributed in a proposal. /// @param movedToVoteTokenCount The total amount of tokens to possibly be distributed in the voting cycle. struct VotingCycleData { - uint256 startingBlock; + uint256 startingTimestamp; uint256 duration; uint256 votingCycleDistributionLimit; uint256 movedToVoteTokenCount; @@ -273,7 +274,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _owner The address that will own the contract. /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _cycleNumber The number of the current voting cycle. - /// @param _startBlock The block number of the starting block of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. @@ -283,7 +284,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address _owner, IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _distributionThreshold, @@ -298,7 +299,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } proposalTypesConfigurator = _proposalTypesConfigurator; - _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setDistributionThreshold(_distributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { @@ -712,10 +713,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if the voting cycle is valid VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - // TODO: is + duration correct? if ( - votingCycleData.startingBlock > block.number - || votingCycleData.startingBlock + votingCycleData.duration < block.number + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp ) { revert ProposalValidator_InvalidVotingCycle(); } @@ -805,10 +805,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if proposal can be moved to vote VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - // TODO: is + duration correct? if ( - votingCycleData.startingBlock > block.number - || votingCycleData.startingBlock + votingCycleData.duration < block.number + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp ) { revert ProposalValidator_InvalidVotingCycle(); } @@ -837,19 +836,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Sets the data of a voting cycle. /// @param _cycleNumber The number of the voting cycle to set. - /// @param _startBlock The block number of the starting block of the voting cycle. - /// @param _duration The duration of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. + /// @param _duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week + /// 3 + /// of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. function setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) external onlyOwner { - _setVotingCycleData(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } /// @notice Sets the max amount of tokens that can be distributed in a proposal. @@ -1021,28 +1022,30 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Private function to set the voting cycle data and emit event. /// @param _cycleNumber The number of the voting cycle to set. - /// @param _startBlock The block number of the starting block of the voting cycle. - /// @param _duration The duration of the voting cycle. + /// @param _startingTimestamp The starting timestamp of the voting cycle. + /// @param _duration The duration of the voting cycle. Should be 1 day which is the end of Week 2 and start of Week + /// 3 + /// of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. function _setVotingCycleData( uint256 _cycleNumber, - uint256 _startBlock, + uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit ) private { - if (votingCycles[_cycleNumber].startingBlock != 0) { + if (votingCycles[_cycleNumber].startingTimestamp != 0) { revert ProposalValidator_VotingCycleAlreadySet(); } votingCycles[_cycleNumber] = VotingCycleData({ - startingBlock: _startBlock, + startingTimestamp: _startingTimestamp, duration: _duration, votingCycleDistributionLimit: _votingCycleDistributionLimit, movedToVoteTokenCount: 0 }); - emit VotingCycleDataSet(_cycleNumber, _startBlock, _duration, _votingCycleDistributionLimit); + emit VotingCycleDataSet(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } /// @notice Private function to set the distribution threshold and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index fd369cf994c..6971a1de14b 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -120,8 +120,8 @@ contract ProposalValidator_Init is CommonTest { using stdStorage for StdStorage; uint256 public constant CYCLE_NUMBER = 1; - uint256 public constant START_BLOCK = 1000000; - uint256 public constant DURATION = 100; + uint256 public constant START_TIMESTAMP = 1000000; + uint256 public constant DURATION = 1 days; uint256 public constant DISTRIBUTION_LIMIT = 20000 ether; uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 1; @@ -155,7 +155,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); event MinimumVotingPowerSet(uint256 newMinimumVotingPower); event VotingCycleDataSet( - uint256 cycleNumber, uint256 startBlock, uint256 duration, uint256 votingCycleDistributionLimit + uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); event DistributionThresholdSet(uint256 newDistributionThreshold); event ProposalTypeDataSet( @@ -242,19 +242,18 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper to create minimal valid arrays for funding proposal error tests - function _createMinimalFundingArrays() + function _createMinimalFundingArrays(uint256 _length) internal - pure returns (string[] memory descriptions_, address[] memory recipients_, uint256[] memory amounts_) { - descriptions_ = new string[](1); - descriptions_[0] = "Option A"; - - recipients_ = new address[](1); - recipients_[0] = address(0x1); - - amounts_ = new uint256[](1); - amounts_[0] = 100 ether; + descriptions_ = new string[](_length); + recipients_ = new address[](_length); + amounts_ = new uint256[](_length); + for (uint256 i = 0; i < _length; i++) { + descriptions_[i] = string.concat("Option ", vm.toString(i + 1)); + recipients_[i] = makeAddr(string.concat("recipient", vm.toString(i + 1))); + amounts_[i] = 100 ether * (i + 1); + } } function _getProposalTypesAndData() @@ -516,7 +515,7 @@ contract ProposalValidator_Init is CommonTest { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -599,7 +598,7 @@ contract ProposalValidator_Init is CommonTest { /// @title ProposalValidator_Version_Test /// @notice Tests for the version function contract ProposalValidator_Version_Test is ProposalValidator_Init { - function test_version_succeeds() public { + function test_version_succeeds() public view { string memory versionString = validator.version(); assertEq(versionString, "1.0.0-beta.1"); } @@ -634,7 +633,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -649,9 +648,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { assertEq(validator.owner(), owner); // Verify voting cycle data - (uint256 startBlock, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = + (uint256 startingTimestamp, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = validator.votingCycles(CYCLE_NUMBER); - assertEq(startBlock, START_BLOCK); + assertEq(startingTimestamp, START_TIMESTAMP); assertEq(duration, DURATION); assertEq(distributionLimit, DISTRIBUTION_LIMIT); assertEq(movedToVoteTokenCount, 0); @@ -706,7 +705,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { owner, proposalTypesConfigurator, CYCLE_NUMBER, - START_BLOCK, + START_TIMESTAMP, DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, @@ -1386,11 +1385,10 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init // Start with minimal arrays and extend based on option count (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(optionCount); + // fuzz the amounts for (uint256 i = 0; i < optionCount; i++) { - descriptions[i] = descriptions[0]; - recipients[i] = recipients[0]; amounts[i] = amount; } @@ -1460,7 +1458,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); @@ -1586,7 +1584,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Create arrays with excessive amount (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); amounts[0] = excessAmount; vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); @@ -1602,7 +1600,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); // Calculate expected proposal hash bytes memory votingModuleData = @@ -1642,7 +1640,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(); + _createMinimalFundingArrays(1); // Calculate expected proposal hash bytes memory votingModuleData = @@ -1921,7 +1919,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public { + function test_canApproveProposal_returnTrue_succeeds() public view { // Attestation already created in setUp bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); assertTrue(canApprove); @@ -2111,7 +2109,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop emit ProposalMovedToVote(expectedHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); @@ -2192,7 +2190,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(block.number + DURATION + 1); + vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } @@ -2214,7 +2212,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } @@ -2294,7 +2292,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, @@ -2334,7 +2332,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); // Move to vote - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, @@ -2364,7 +2362,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat function setUp() public override { super.setUp(); - (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(); + (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(1); (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, @@ -2546,7 +2544,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); - vm.roll(START_BLOCK + DURATION + 1); + vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType @@ -2622,7 +2620,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(approvedProposer); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); validator.moveToVoteFundingProposal( criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType ); @@ -2664,7 +2662,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); - vm.roll(START_BLOCK + 1); + vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType @@ -2677,7 +2675,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setVotingCycleData_succeeds( uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2687,19 +2685,19 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { // Expect the VotingCycleDataSet event to be emitted vm.expectEmit(address(validator)); - emit VotingCycleDataSet(cycleNumber, startBlock, duration, distributionLimit); + emit VotingCycleDataSet(cycleNumber, startingTimestamp, duration, distributionLimit); vm.prank(owner); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); ( - uint256 actualStartBlock, + uint256 actualStartingTimestamp, uint256 actualDuration, uint256 actualDistributionLimit, uint256 actualMovedToVoteTokenCount ) = validator.votingCycles(cycleNumber); - assertEq(actualStartBlock, startBlock); + assertEq(actualStartingTimestamp, startingTimestamp); assertEq(actualDuration, duration); assertEq(actualDistributionLimit, distributionLimit); assertEq(actualMovedToVoteTokenCount, 0); @@ -2708,7 +2706,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { function testFuzz_setVotingCycleData_notOwner_reverts( address caller, uint256 cycleNumber, - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2718,11 +2716,11 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setVotingCycleData(cycleNumber, startBlock, duration, distributionLimit); + validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); } function testFuzz_setVotingCycleData_votingCycleAlreadySet_reverts( - uint256 startBlock, + uint256 startingTimestamp, uint256 duration, uint256 distributionLimit ) @@ -2730,7 +2728,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { { vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER, startBlock, duration, distributionLimit); + validator.setVotingCycleData(CYCLE_NUMBER, startingTimestamp, duration, distributionLimit); } function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { @@ -2801,6 +2799,7 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init bytes32 descriptionHash ) public + view { bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); bytes32 expectedHash = From 41c9d3d4539f8bdbd36da98ffc5ddc983840be13 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:13:28 +0300 Subject: [PATCH 54/73] fix: remove imported contracts (#443) * fix: voting window to use timestamp * fix: pre-pr * fix: improve test * fix: remove contracts and import interfaces * fix: snapshots * fix: remove unused state variable * fix: pre-pr * fix: change property naming --- .semgrep/rules/sol-rules.yaml | 23 - .../governance/IApprovalVotingModule.sol | 28 + .../governance/IOptimismGovernor.sol | 3 +- .../governance/IOptimisticModule.sol | 12 + .../governance/IProposalValidator.sol | 14 +- .../snapshots/abi/ApprovalVotingModule.json | 346 ------------ .../snapshots/abi/OptimisticModule.json | 258 --------- .../snapshots/abi/ProposalValidator.json | 76 +-- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ApprovalVotingModule.json | 16 - .../storageLayout/OptimisticModule.json | 9 - .../storageLayout/ProposalValidator.json | 2 +- .../src/governance/ApprovalVotingModule.sol | 522 ------------------ .../src/governance/OptimisticModule.sol | 154 ------ .../src/governance/ProposalValidator.sol | 103 ++-- .../src/governance/VotingModule.sol | 73 --- .../test/governance/ProposalValidator.t.sol | 90 ++- 17 files changed, 161 insertions(+), 1572 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol create mode 100644 packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol delete mode 100644 packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json delete mode 100644 packages/contracts-bedrock/snapshots/abi/OptimisticModule.json delete mode 100644 packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json delete mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json delete mode 100644 packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol delete mode 100644 packages/contracts-bedrock/src/governance/OptimisticModule.sol delete mode 100644 packages/contracts-bedrock/src/governance/VotingModule.sol diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index ec576a400c9..773ecc8b7d0 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -45,12 +45,7 @@ rules: - pattern: vm.expectRevert() paths: exclude: -<<<<<<< HEAD - packages/contracts-bedrock/test/universal/WETH98.t.sol -======= - - packages/contracts-bedrock/test/dispute/WETH98.t.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol ->>>>>>> feat: add submit funding proposal (#411) - id: sol-safety-natspec-semver-match languages: [generic] @@ -133,7 +128,6 @@ rules: - packages/contracts-bedrock/src/governance/GovernanceToken.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-return-arg-fmt languages: [solidity] @@ -160,7 +154,6 @@ rules: exclude: - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - id: sol-style-malformed-require languages: [solidity] @@ -178,14 +171,6 @@ rules: exclude: - packages/contracts-bedrock/src/libraries/Bytes.sol - packages/contracts-bedrock/src/legacy/LegacyMintableERC20.sol -<<<<<<< HEAD -======= - - packages/contracts-bedrock/src/cannon/MIPS.sol - - packages/contracts-bedrock/src/cannon/MIPS2.sol - - packages/contracts-bedrock/src/cannon/libraries/MIPSMemory.sol - - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol ->>>>>>> feat: add submit funding proposal (#411) - id: sol-style-malformed-revert languages: [solidity] @@ -199,13 +184,6 @@ rules: - pattern-not-regex: string\.concat\(\"(\w+:\s[^"]*)\"\,.+\) - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\" - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\" -<<<<<<< HEAD -======= - paths: - exclude: - - packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol - - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol ->>>>>>> feat: add submit funding proposal (#411) - id: sol-style-use-abi-encodecall languages: [solidity] @@ -266,7 +244,6 @@ rules: - packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol - packages/contracts-bedrock/src/governance/VotingModule.sol - packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol - - packages/contracts-bedrock/src/governance/OptimisticModule.sol - packages/contracts-bedrock/src/cannon/libraries/MIPS64Instructions.sol - packages/contracts-bedrock/src/cannon/libraries/CannonErrors.sol - packages/contracts-bedrock/src/L2/SuperchainETHBridge.sol diff --git a/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol b/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol new file mode 100644 index 00000000000..f17217e8730 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IApprovalVotingModule.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title IApprovalVotingModule +/// @notice Interface for the Approval Voting Module containing only the essential types +/// needed by the ProposalValidator contract. +interface IApprovalVotingModule { + struct ProposalOption { + uint256 budgetTokensSpent; + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + } + + struct ProposalSettings { + uint8 maxApprovals; + uint8 criteria; + address budgetToken; + uint128 criteriaValue; + uint128 budgetAmount; + } + + enum PassingCriteria { + Threshold, + TopChoices + } +} diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol index 994bb597df4..dfd1af85fed 100644 --- a/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol +++ b/packages/contracts-bedrock/interfaces/governance/IOptimismGovernor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {VotingModule} from "src/governance/VotingModule.sol"; import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; interface IOptimismGovernor { @@ -14,7 +13,7 @@ interface IOptimismGovernor { ) external returns (uint256 proposalId); function proposeWithModule( - VotingModule module, + address module, bytes memory proposalData, string memory description, uint8 proposalType diff --git a/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol b/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol new file mode 100644 index 00000000000..8b03a9986df --- /dev/null +++ b/packages/contracts-bedrock/interfaces/governance/IOptimisticModule.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title IOptimisticModule +/// @notice Interface for the Optimistic Module containing only the essential types +/// needed by the ProposalValidator contract. +interface IOptimisticModule { + struct ProposalSettings { + uint248 againstThreshold; + bool isRelativeToVotableSupply; + } +} \ No newline at end of file diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 1693007ce3a..5c0c48a7be4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; // Interfaces -import {IGovernanceToken} from './IGovernanceToken.sol'; import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; @@ -56,7 +55,7 @@ interface IProposalValidator is ISemver { uint256 votingCycleDistributionLimit ); - event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( ProposalType proposalType, @@ -159,7 +158,7 @@ interface IProposalValidator is ISemver { uint256 _votingCycleDistributionLimit ) external; - function setDistributionThreshold(uint256 _distributionThreshold) external; + function setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) external; function setProposalTypeData( ProposalType _proposalType, @@ -173,7 +172,7 @@ interface IProposalValidator is ISemver { uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, - uint256 _distributionThreshold, + uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -182,9 +181,7 @@ interface IProposalValidator is ISemver { function transferOwnership(address newOwner) external; - function distributionThreshold() external view returns (uint256); - - function VOTING_TOKEN() external view returns (IGovernanceToken); + function proposalDistributionThreshold() external view returns (uint256); function GOVERNOR() external view returns (IOptimismGovernor); @@ -212,7 +209,6 @@ interface IProposalValidator is ISemver { function __constructor__( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _votingToken + IOptimismGovernor _governor ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json deleted file mode 100644 index d531b81bb48..00000000000 --- a/packages/contracts-bedrock/snapshots/abi/ApprovalVotingModule.json +++ /dev/null @@ -1,346 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_governor", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "COUNTING_MODE", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "PROPOSAL_DATA_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "VOTE_PARAMS_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - }, - { - "internalType": "uint256", - "name": "budgetTokensSpent", - "type": "uint256" - } - ], - "name": "_afterExecute", - "outputs": [], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint8", - "name": "support", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "weight", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "params", - "type": "bytes" - } - ], - "name": "_countVote", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - } - ], - "name": "_formatExecuteParams", - "outputs": [ - { - "internalType": "address[]", - "name": "targets", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "calldatas", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - } - ], - "name": "_voteSucceeded", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "getAccountTotalVotes", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "getAccountVotes", - "outputs": [ - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "proposals", - "outputs": [ - { - "internalType": "address", - "name": "governor", - "type": "address" - }, - { - "internalType": "uint256", - "name": "initBalance", - "type": "uint256" - }, - { - "components": [ - { - "internalType": "uint8", - "name": "maxApprovals", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "criteria", - "type": "uint8" - }, - { - "internalType": "address", - "name": "budgetToken", - "type": "address" - }, - { - "internalType": "uint128", - "name": "criteriaValue", - "type": "uint128" - }, - { - "internalType": "uint128", - "name": "budgetAmount", - "type": "uint128" - } - ], - "internalType": "struct ProposalSettings", - "name": "settings", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "proposalData", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "descriptionHash", - "type": "bytes32" - } - ], - "name": "propose", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "AlreadyVoted", - "type": "error" - }, - { - "inputs": [], - "name": "BudgetExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "ExistingProposal", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidParams", - "type": "error" - }, - { - "inputs": [], - "name": "MaxApprovalsExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "MaxChoicesExceeded", - "type": "error" - }, - { - "inputs": [], - "name": "NotGovernor", - "type": "error" - }, - { - "inputs": [], - "name": "OptionsNotStrictlyAscending", - "type": "error" - }, - { - "inputs": [], - "name": "WrongProposalId", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json b/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json deleted file mode 100644 index f2e29a066bd..00000000000 --- a/packages/contracts-bedrock/snapshots/abi/OptimisticModule.json +++ /dev/null @@ -1,258 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_governor", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "COUNTING_MODE", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "PERCENT_DIVISOR", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "PROPOSAL_DATA_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "VOTE_PARAMS_ENCODING", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "uint8", - "name": "", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "name": "_countVote", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "name": "_formatExecuteParams", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "", - "type": "bytes[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" - } - ], - "name": "_voteSucceeded", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "proposals", - "outputs": [ - { - "internalType": "address", - "name": "governor", - "type": "address" - }, - { - "components": [ - { - "internalType": "uint248", - "name": "againstThreshold", - "type": "uint248" - }, - { - "internalType": "bool", - "name": "isRelativeToVotableSupply", - "type": "bool" - } - ], - "internalType": "struct ProposalSettings", - "name": "settings", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_proposalId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "_proposalData", - "type": "bytes" - }, - { - "internalType": "bytes32", - "name": "_descriptionHash", - "type": "bytes32" - } - ], - "name": "propose", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "AlreadyVoted", - "type": "error" - }, - { - "inputs": [], - "name": "ExistingProposal", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidParams", - "type": "error" - }, - { - "inputs": [], - "name": "NotGovernor", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_NotOptimisticProposalType", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_OptimisticModuleOnlySignal", - "type": "error" - }, - { - "inputs": [], - "name": "OptimisticModule_WrongProposalId", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 40ba0366dd0..03a0733070c 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -15,11 +15,6 @@ "internalType": "contract IOptimismGovernor", "name": "_governor", "type": "address" - }, - { - "internalType": "contract IGovernanceToken", - "name": "_votingToken", - "type": "address" } ], "stateMutability": "nonpayable", @@ -77,19 +72,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "VOTING_TOKEN", - "outputs": [ - { - "internalType": "contract IGovernanceToken", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -132,19 +114,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "distributionThreshold", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", @@ -192,7 +161,7 @@ }, { "internalType": "uint256", - "name": "_distributionThreshold", + "name": "_proposalDistributionThreshold", "type": "uint256" }, { @@ -333,6 +302,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proposalDistributionThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "proposalTypesConfigurator", @@ -381,11 +363,11 @@ "inputs": [ { "internalType": "uint256", - "name": "_distributionThreshold", + "name": "_proposalDistributionThreshold", "type": "uint256" } ], - "name": "setDistributionThreshold", + "name": "setProposalDistributionThreshold", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -635,19 +617,6 @@ "stateMutability": "view", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newDistributionThreshold", - "type": "uint256" - } - ], - "name": "DistributionThresholdSet", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -699,6 +668,19 @@ "name": "ProposalApproved", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newProposalDistributionThreshold", + "type": "uint256" + } + ], + "name": "ProposalDistributionThresholdSet", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 37956c0574c..27c970a2404 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xc4efda2929244bf984fd5a3e32b6a8b5fb68622af6b05a31d3e5f7a25cd6bd3b", - "sourceCodeHash": "0x0064ec36b626190c1d2460aac284df6eaddcb51bf03ff60fa45aecad4ea922c6" + "initCodeHash": "0xe7a93826772bf108a21923f7e45b1f46cdadb75e48b0c796e43d64f0c1d81504", + "sourceCodeHash": "0xadd8e049bf3c652af123b6c64a1d504c92be13b4797ab10b978df907b05dcf7f" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json b/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json deleted file mode 100644 index 43e2fd35e17..00000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/ApprovalVotingModule.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "bytes": "32", - "label": "proposals", - "offset": 0, - "slot": "0", - "type": "mapping(uint256 => struct Proposal)" - }, - { - "bytes": "32", - "label": "accountVotesSet", - "offset": 0, - "slot": "1", - "type": "mapping(uint256 => mapping(address => struct EnumerableSetUpgradeable.UintSet))" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json deleted file mode 100644 index a600d98d300..00000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimisticModule.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "bytes": "32", - "label": "proposals", - "offset": 0, - "slot": "0", - "type": "mapping(uint256 => struct Proposal)" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 50c94894f8b..2c01297b2b0 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -43,7 +43,7 @@ }, { "bytes": "32", - "label": "distributionThreshold", + "label": "proposalDistributionThreshold", "offset": 0, "slot": "102", "type": "uint256" diff --git a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol b/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol deleted file mode 100644 index 7f189d3f6ec..00000000000 --- a/packages/contracts-bedrock/src/governance/ApprovalVotingModule.sol +++ /dev/null @@ -1,522 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { EnumerableSetUpgradeable } from - "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; -import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { VotingModule } from "./VotingModule.sol"; - -enum VoteType { - Against, - For, - Abstain -} - -enum PassingCriteria { - Threshold, - TopChoices -} - -struct ExecuteParams { - address targets; - uint256 values; - bytes calldatas; -} - -struct ProposalSettings { - uint8 maxApprovals; - uint8 criteria; - address budgetToken; - uint128 criteriaValue; - uint128 budgetAmount; -} - -struct ProposalOption { - uint256 budgetTokensSpent; - address[] targets; - uint256[] values; - bytes[] calldatas; - string description; -} - -struct Proposal { - address governor; - uint256 initBalance; - uint128[] optionVotes; - ProposalOption[] options; - ProposalSettings settings; -} - -contract ApprovalVotingModule is VotingModule { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error WrongProposalId(); - error MaxChoicesExceeded(); - error MaxApprovalsExceeded(); - error BudgetExceeded(); - error OptionsNotStrictlyAscending(); - - /*////////////////////////////////////////////////////////////// - LIBRARIES - //////////////////////////////////////////////////////////////*/ - - using SafeCastLib for uint256; - using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => Proposal) public proposals; - mapping(uint256 => mapping(address => EnumerableSetUpgradeable.UintSet)) private accountVotesSet; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) VotingModule(_governor) { } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * Save settings and options for a new proposal. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. - */ - function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external override { - _onlyGovernor(); - if (proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), proposalData, descriptionHash)))) { - revert WrongProposalId(); - } - - if (proposals[proposalId].governor != address(0)) { - revert ExistingProposal(); - } - - (ProposalOption[] memory proposalOptions, ProposalSettings memory proposalSettings) = - abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - uint256 optionsLength = proposalOptions.length; - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert InvalidParams(); - } - if (proposalSettings.criteria == uint8(PassingCriteria.TopChoices)) { - if (proposalSettings.criteriaValue > optionsLength) { - revert MaxChoicesExceeded(); - } - } - - unchecked { - // Ensure proposal params of each option have the same length between themselves - ProposalOption memory option; - for (uint256 i; i < optionsLength; ++i) { - option = proposalOptions[i]; - if (option.targets.length != option.values.length || option.targets.length != option.calldatas.length) { - revert InvalidParams(); - } - - proposals[proposalId].options.push(option); - } - } - - proposals[proposalId].governor = msg.sender; - proposals[proposalId].settings = proposalSettings; - proposals[proposalId].optionVotes = new uint128[](optionsLength); - } - - /** - * Count approvals voted by `account`. If voting for, options need to be set in ascending order. Votes can only be - * cast once. - * - * @param proposalId The id of the proposal. - * @param account The account to count votes for. - * @param support The type of vote to count. - * @param weight The total vote weight of the `account`. - * @param params The ids of the options to vote for sorted in ascending order, encoded as `uint256[]`. - */ - function _countVote( - uint256 proposalId, - address account, - uint8 support, - uint256 weight, - bytes memory params - ) - external - virtual - override - { - _onlyGovernor(); - Proposal memory proposal = proposals[proposalId]; - - if (support == uint8(VoteType.For)) { - if (weight != 0) { - uint256[] memory options = _decodeVoteParams(params); - uint256 totalOptions = options.length; - if (totalOptions == 0) revert InvalidParams(); - - _recordVote( - proposalId, account, weight.toUint128(), options, totalOptions, proposal.settings.maxApprovals - ); - } - } - } - - /** - * Format executeParams for a governor, given `proposalId` and `proposalData`. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. - * @return targets The targets of the proposal. - * @return values The values of the proposal. - * @return calldatas The calldatas of the proposal. - */ - function _formatExecuteParams( - uint256 proposalId, - bytes memory proposalData - ) - public - override - returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) - { - _onlyGovernor(); - (ProposalOption[] memory options, ProposalSettings memory settings) = - abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - { - IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); - - // If budgetToken is not ETH - if (settings.budgetToken != address(0)) { - // Save initBalance to be used as comparison in `_afterExecute` - proposals[proposalId].initBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); - } - } - - (uint128[] memory sortedOptionVotes, ProposalOption[] memory sortedOptions) = - _sortOptions(proposals[proposalId].optionVotes, options); - - (uint256 executeParamsLength, uint256 succeededOptionsLength) = - _countOptions(sortedOptions, sortedOptionVotes, settings); - - ExecuteParams[] memory executeParams = new ExecuteParams[](executeParamsLength); - executeParamsLength = 0; - uint256 n; - uint256 totalValue; - ProposalOption memory option; - - { - bool budgetExceeded = false; - - // Flatten `options` by filling `executeParams` until budgetAmount is exceeded - for (uint256 i; i < succeededOptionsLength;) { - option = sortedOptions[i]; - - for (n = 0; n < option.targets.length;) { - // If `budgetToken` is ETH and value is not zero, add transaction value to `totalValue` - if (settings.budgetToken == address(0) && option.values[n] != 0) { - if (totalValue + option.values[n] > settings.budgetAmount) { - budgetExceeded = true; - break; // break inner loop - } - totalValue += option.values[n]; - } - - unchecked { - executeParams[executeParamsLength + n] = - ExecuteParams(option.targets[n], option.values[n], option.calldatas[n]); - - ++n; - } - } - - // If `budgetAmount` for ETH is exceeded, skip option. - if (budgetExceeded) break; - - // Check if budgetAmount is exceeded for non-ETH tokens - if (settings.budgetToken != address(0) && settings.budgetAmount != 0) { - if (option.budgetTokensSpent != 0) { - if (totalValue + option.budgetTokensSpent > settings.budgetAmount) break; // break outer loop - // for non-ETH tokens - totalValue += option.budgetTokensSpent; - } - } - - unchecked { - executeParamsLength += n; - - ++i; - } - } - } - - unchecked { - // Increase by one to account for additional `_afterExecute` call - uint256 effectiveParamsLength = executeParamsLength + 1; - - // Init params lengths - targets = new address[](effectiveParamsLength); - values = new uint256[](effectiveParamsLength); - calldatas = new bytes[](effectiveParamsLength); - } - - // Set n `targets`, `values` and `calldatas` - for (uint256 i; i < executeParamsLength;) { - targets[i] = executeParams[i].targets; - values[i] = executeParams[i].values; - calldatas[i] = executeParams[i].calldatas; - - unchecked { - ++i; - } - } - - // Set `_afterExecute` as last call - targets[executeParamsLength] = address(this); - values[executeParamsLength] = 0; - calldatas[executeParamsLength] = - abi.encodeWithSelector(this._afterExecute.selector, proposalId, proposalData, totalValue); - } - - /** - * Hook called by a governor after execute, for `proposalId` with `proposalData`. - * Revert if the transaction has resulted in more tokens being spent than `budgetAmount`. - * - * @param proposalId The id of the proposal. - * @param proposalData The proposal data encoded as `(ProposalOption[], ProposalSettings)`. - * @param budgetTokensSpent The total amount of tokens that can be spent. - */ - function _afterExecute(uint256 proposalId, bytes memory proposalData, uint256 budgetTokensSpent) public view { - (, ProposalSettings memory settings) = abi.decode(proposalData, (ProposalOption[], ProposalSettings)); - - if (settings.budgetToken != address(0) && settings.budgetAmount > 0) { - IOptimismGovernor governor = IOptimismGovernor(proposals[proposalId].governor); - - uint256 initBalance = proposals[proposalId].initBalance; - uint256 finalBalance = IERC20(settings.budgetToken).balanceOf(governor.timelock()); - - // If `finalBalance` is higher than `initBalance`, ignore the budget check - if (finalBalance < initBalance) { - /// @dev Cannot underflow as `finalBalance` is less than `initBalance` - unchecked { - if (initBalance - finalBalance > budgetTokensSpent) { - revert BudgetExceeded(); - } - } - } - } - } - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * Return the ids of the options voted by `account` on `proposalId`. - */ - function getAccountVotes(uint256 proposalId, address account) external view returns (uint256[] memory) { - return accountVotesSet[proposalId][account].values(); - } - - /** - * Return the total number of votes cast by `account` on `proposalId`. - */ - function getAccountTotalVotes(uint256 proposalId, address account) external view returns (uint256) { - return accountVotesSet[proposalId][account].length(); - } - - /** - * @dev Return true if at least one option satisfies the passing criteria. - * Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. - * - * @param proposalId The id of the proposal. - */ - function _voteSucceeded(uint256 proposalId) external view override returns (bool) { - Proposal memory proposal = proposals[proposalId]; - - ProposalOption[] memory options = proposal.options; - uint256 n = options.length; - unchecked { - if (proposal.settings.criteria == uint8(PassingCriteria.Threshold)) { - for (uint256 i; i < n; ++i) { - if (proposal.optionVotes[i] >= proposal.settings.criteriaValue) return true; - } - } else if (proposal.settings.criteria == uint8(PassingCriteria.TopChoices)) { - for (uint256 i; i < n; ++i) { - if (proposal.optionVotes[i] != 0) return true; - } - } - } - - return false; - } - - /** - * Defines the encoding for the expected `proposalData` in `propose`. - * Encoding: `(ProposalOption[], ProposalSettings)` - * - * @dev Can be used by clients to interact with modules programmatically without prior knowledge - * on expected types. - */ - function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { - return - "((uint256 budgetTokensSpent,address[] targets,uint256[] values,bytes[] calldatas,string description)[] proposalOptions,(uint8 maxApprovals,uint8 criteria,address budgetToken,uint128 criteriaValue,uint128 budgetAmount) proposalSettings)"; - } - - /** - * Defines the encoding for the expected `params` in `_countVote`. - * Encoding: `uint256[]` - * - * @dev Can be used by clients to interact with modules programmatically without prior knowledge - * on expected types. - */ - function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { - return "uint256[] optionIds"; - } - - /** - * @dev See {IGovernor-COUNTING_MODE}. - * - * - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - * - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. - * - `params=approvalVote`: params needs to be formatted as `VOTE_PARAMS_ENCODING`. - */ - function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo&quorum=against,for,abstain¶ms=approvalVote"; - } - - /** - * Module version. - */ - function version() public pure returns (uint256) { - return 1; - } - - /*////////////////////////////////////////////////////////////// - INTERNAL - //////////////////////////////////////////////////////////////*/ - - function _recordVote( - uint256 proposalId, - address account, - uint128 weight, - uint256[] memory options, - uint256 totalOptions, - uint256 maxApprovals - ) - internal - { - uint256 option; - uint256 prevOption; - for (uint256 i; i < totalOptions;) { - option = options[i]; - - accountVotesSet[proposalId][account].add(option); - - // Revert if `option` is not strictly ascending - if (i != 0) { - if (option <= prevOption) revert OptionsNotStrictlyAscending(); - } - - prevOption = option; - - /// @dev Revert if `option` is out of bounds - proposals[proposalId].optionVotes[option] += weight; - - unchecked { - ++i; - } - } - - if (accountVotesSet[proposalId][account].length() > maxApprovals) { - revert MaxApprovalsExceeded(); - } - } - - // Sort `options` by `optionVotes` in descending order - function _sortOptions( - uint128[] memory optionVotes, - ProposalOption[] memory options - ) - internal - pure - returns (uint128[] memory, ProposalOption[] memory) - { - unchecked { - uint128 highestValue; - ProposalOption memory highestOption; - uint256 index; - - for (uint256 i; i < optionVotes.length - 1; ++i) { - highestValue = optionVotes[i]; - - for (uint256 j = i + 1; j < optionVotes.length; ++j) { - if (optionVotes[j] > highestValue) { - highestValue = optionVotes[j]; - index = j; - } - } - - if (index != 0) { - optionVotes[index] = optionVotes[i]; - optionVotes[i] = highestValue; - - highestOption = options[index]; - options[index] = options[i]; - options[i] = highestOption; - - index = 0; - } - } - - return (optionVotes, options); - } - } - - // Derive `executeParamsLength` and `succeededOptionsLength` based on passing criteria - function _countOptions( - ProposalOption[] memory options, - uint128[] memory optionVotes, - ProposalSettings memory settings - ) - internal - pure - returns (uint256 executeParamsLength, uint256 succeededOptionsLength) - { - uint256 n = options.length; - unchecked { - uint256 i; - if (settings.criteria == uint8(PassingCriteria.Threshold)) { - // if criteria is `Threshold`, loop through options until `optionVotes` is less than threshold - for (i; i < n; ++i) { - if (optionVotes[i] >= settings.criteriaValue) { - executeParamsLength += options[i].targets.length; - } else { - break; - } - } - } else if (settings.criteria == uint8(PassingCriteria.TopChoices)) { - // if criteria is `TopChoices`, loop through options until the top choices are filled - for (i; i < settings.criteriaValue; ++i) { - if (optionVotes[i] > 0) { - executeParamsLength += options[i].targets.length; - } else { - break; - } - } - } - succeededOptionsLength = i; - } - } - - // Virtual method used to decode _countVote params. - function _decodeVoteParams(bytes memory params) internal virtual returns (uint256[] memory options) { - options = abi.decode(params, (uint256[])); - } -} diff --git a/packages/contracts-bedrock/src/governance/OptimisticModule.sol b/packages/contracts-bedrock/src/governance/OptimisticModule.sol deleted file mode 100644 index 736b73cbb57..00000000000 --- a/packages/contracts-bedrock/src/governance/OptimisticModule.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { IGovernorUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol"; -import { IVotesUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; -import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; -import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { VotingModule } from "./VotingModule.sol"; - -enum VoteType { - Against, - For, - Abstain -} - -struct ProposalSettings { - uint248 againstThreshold; - bool isRelativeToVotableSupply; -} - -struct Proposal { - address governor; - ProposalSettings settings; -} - -contract OptimisticModule is VotingModule { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error OptimisticModule_WrongProposalId(); - error OptimisticModule_NotOptimisticProposalType(); - error OptimisticModule_OptimisticModuleOnlySignal(); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - uint16 public constant PERCENT_DIVISOR = 10_000; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => Proposal) public proposals; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) VotingModule(_governor) { } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Validate proposal is optimistic and save settings for a new proposal. - /// @param _proposalId The id of the proposal. - /// @param _proposalData The proposal data encoded as `PROPOSAL_DATA_ENCODING`. - function propose(uint256 _proposalId, bytes memory _proposalData, bytes32 _descriptionHash) external override { - _onlyGovernor(); - if (_proposalId != uint256(keccak256(abi.encode(msg.sender, address(this), _proposalData, _descriptionHash)))) { - revert OptimisticModule_WrongProposalId(); - } - - if (proposals[_proposalId].governor != address(0)) { - revert ExistingProposal(); - } - - ProposalSettings memory proposalSettings = abi.decode(_proposalData, (ProposalSettings)); - - uint8 proposalTypeId = IOptimismGovernor(msg.sender).getProposalType(_proposalId); - IProposalTypesConfigurator proposalConfigurator = - IProposalTypesConfigurator(IOptimismGovernor(msg.sender).PROPOSAL_TYPES_CONFIGURATOR()); - IProposalTypesConfigurator.ProposalType memory proposalType = proposalConfigurator.proposalTypes(proposalTypeId); - - if (proposalType.quorum != 0 || proposalType.approvalThreshold != 0) { - revert OptimisticModule_NotOptimisticProposalType(); - } - if ( - proposalSettings.againstThreshold == 0 - || (proposalSettings.isRelativeToVotableSupply && proposalSettings.againstThreshold > PERCENT_DIVISOR) - ) { - revert InvalidParams(); - } - - proposals[_proposalId].governor = msg.sender; - proposals[_proposalId].settings = proposalSettings; - } - - /// @notice Counting logic is skipped. - function _countVote(uint256, address, uint8, uint256, bytes memory) external virtual override { } - - /// @notice Reverts to prevent queue and execute of proposals with optimistic module. - function _formatExecuteParams( - uint256, - bytes memory - ) - public - pure - override - returns (address[] memory, uint256[] memory, bytes[] memory) - { - revert OptimisticModule_OptimisticModuleOnlySignal(); - } - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @dev Return true if `againstVotes` is lower than `againstThreshold`. - /// Used by governor in `_voteSucceeded`. See {Governor-_voteSucceeded}. - /// @param _proposalId The id of the proposal. - function _voteSucceeded(uint256 _proposalId) external view override returns (bool) { - Proposal memory proposal = proposals[_proposalId]; - (uint256 againstVotes,,) = IOptimismGovernor(proposal.governor).proposalVotes(_proposalId); - - uint256 againstThreshold = proposal.settings.againstThreshold; - if (proposal.settings.isRelativeToVotableSupply) { - uint256 snapshotBlock = IGovernorUpgradeable(proposal.governor).proposalSnapshot(_proposalId); - IVotesUpgradeable token = IOptimismGovernor(proposal.governor).token(); - againstThreshold = (token.getPastTotalSupply(snapshotBlock) * againstThreshold) / PERCENT_DIVISOR; - } - - return againstVotes < againstThreshold; - } - - /// @dev Defines the encoding for the expected `proposalData` in `propose`. - /// Encoding: `(ProposalSettings)` - /// Can be used by clients to interact with modules programmatically without prior knowledge - /// on expected types. - function PROPOSAL_DATA_ENCODING() external pure virtual override returns (string memory) { - return "((uint248 againstThreshold,bool isRelativeToVotableSupply) proposalSettings)"; - } - - /// @dev Defines the encoding for the expected `params` in `_countVote`. - /// Can be used by clients to interact with modules programmatically without prior knowledge - /// on expected types. - function VOTE_PARAMS_ENCODING() external pure virtual override returns (string memory) { - return ""; - } - - /// @dev See {IGovernor-COUNTING_MODE}. - /// - `support=bravo`: Supports vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. - /// - `quorum=for,abstain`: Against, For and Abstain votes are counted towards quorum. - function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo&quorum=against,for,abstain"; - } - - /// @notice Module version. - function version() public pure returns (uint256) { - return 1; - } -} diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 1aa237ef4be..d9ffaafe21b 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -10,20 +10,12 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; - -// Modules -import { - ProposalSettings as ApprovalProposalSettings, - ProposalOption, - PassingCriteria -} from "src/governance/ApprovalVotingModule.sol"; -import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; -import { VotingModule } from "src/governance/VotingModule.sol"; +import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; +import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; /// @custom:proxied true /// @title ProposalValidator @@ -126,9 +118,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); - /// @notice Emitted when the distribution threshold is set. - /// @param newDistributionThreshold The new distribution threshold. - event DistributionThresholdSet(uint256 newDistributionThreshold); + /// @notice Emitted when the proposal distribution limit is set. + /// @param newProposalDistributionThreshold The new proposal distribution threshold. + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. @@ -225,14 +217,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The governance token contract. - IGovernanceToken public immutable VOTING_TOKEN; - /// @notice The proposal types configurator contract. IProposalTypesConfigurator public proposalTypesConfigurator; /// @notice The max amount of tokens that can be distributed in a proposal. - uint256 public distributionThreshold; + uint256 public proposalDistributionThreshold; /// @notice Mapping of voting cycle numbers to their corresponding data. mapping(uint256 => VotingCycleData) public votingCycles; @@ -244,9 +233,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { mapping(bytes32 => ProposalData) internal _proposals; /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 1.0.0 function version() public pure virtual returns (string memory) { - return "1.0.0-beta.1"; + return "1.0.0"; } /// @notice Constructs the ProposalValidator contract. @@ -254,19 +243,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. - /// @param _votingToken The token used to determine voting power. constructor( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _votingToken + IOptimismGovernor _governor ) ReinitializableBase(1) { APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; GOVERNOR = _governor; - VOTING_TOKEN = _votingToken; _disableInitializers(); } @@ -277,7 +263,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. - /// @param _distributionThreshold The max amount of tokens that can be distributed in a proposal. + /// @param _proposalDistributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set data for. /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( @@ -287,7 +273,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _startingTimestamp, uint256 _duration, uint256 _votingCycleDistributionLimit, - uint256 _distributionThreshold, + uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -300,7 +286,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposalTypesConfigurator = _proposalTypesConfigurator; _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); - _setDistributionThreshold(_distributionThreshold); + _setProposalDistributionThreshold(_proposalDistributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { _setProposalTypeData(_proposalTypes[i], _proposalTypesData[i]); @@ -343,7 +329,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Create OptimisticModule ProposalSettings with required parameters - OptimisticProposalSettings memory optimisticSettings = OptimisticProposalSettings({ + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true // MUST always be true }); @@ -384,7 +370,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) ); emit ProposalMovedToVote(proposalHash_, msg.sender); @@ -424,13 +410,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Build proposal options (elections don't execute operations) - (ProposalOption[] memory options,) = + (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections criteriaValue: _criteriaValue, budgetAmount: 0 // No budget amount for elections @@ -511,13 +497,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Build proposal options with funding execution data - (ProposalOption[] memory options, uint256 totalBudget) = + (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, budgetAmount: uint128(totalBudget) @@ -600,8 +586,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Configure optimistic proposal settings - OptimisticProposalSettings memory settings = - OptimisticProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); + IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); bytes memory proposalVotingModuleData = abi.encode(settings); @@ -641,7 +627,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash @@ -666,13 +652,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (bytes32 proposalHash_) { // Configure approval module options - (ProposalOption[] memory options,) = + (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: _criteriaValue, budgetAmount: 0 @@ -724,7 +710,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _proposalDescription, uint8(proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) ); // Make sure the proposalId is the same as the proposalHash @@ -765,13 +751,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Configure approval module options - (ProposalOption[] memory options, uint256 totalBudget) = + (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval module settings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, budgetAmount: uint128(totalBudget) @@ -822,9 +808,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingCycles[proposal.votingCycle].movedToVoteTokenCount += totalBudget; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - VotingModule(votingModule), proposalVotingModuleData, _description, uint8(_proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, uint8(_proposalType)); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -854,9 +839,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Sets the max amount of tokens that can be distributed in a proposal. - /// @param _distributionThreshold The new distribution threshold. - function setDistributionThreshold(uint256 _distributionThreshold) external onlyOwner { - _setDistributionThreshold(_distributionThreshold); + /// @param _proposalDistributionThreshold The new proposal distribution threshold. + function setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) external onlyOwner { + _setProposalDistributionThreshold(_proposalDistributionThreshold); } /// @notice Sets the data for a proposal type. @@ -960,10 +945,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ) internal view - returns (ProposalOption[] memory options_, uint256 totalBudget_) + returns (IApprovalVotingModule.ProposalOption[] memory options_, uint256 totalBudget_) { uint256 optionsLength = _optionDescriptions.length; - options_ = new ProposalOption[](optionsLength); + options_ = new IApprovalVotingModule.ProposalOption[](optionsLength); for (uint256 i = 0; i < optionsLength; i++) { address[] memory targets; @@ -974,7 +959,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Check if this is a funding proposal (has recipients and amounts) if (_recipients.length > 0 && _amounts.length > 0) { // Validate amount doesn't exceed distribution threshold - if (_amounts[i] > distributionThreshold) { + if (_amounts[i] > proposalDistributionThreshold) { revert ProposalValidator_ExceedsDistributionThreshold(); } targets = new address[](1); @@ -993,7 +978,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetTokensSpent = 0; } - options_[i] = ProposalOption({ + options_[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: budgetTokensSpent, targets: targets, values: values, @@ -1048,11 +1033,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit VotingCycleDataSet(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); } - /// @notice Private function to set the distribution threshold and emit event. - /// @param _distributionThreshold The new distribution threshold. - function _setDistributionThreshold(uint256 _distributionThreshold) private { - distributionThreshold = _distributionThreshold; - emit DistributionThresholdSet(_distributionThreshold); + /// @notice Private function to set the proposal distribution threshold and emit event. + /// @param _proposalDistributionThreshold The new proposal distribution threshold. + function _setProposalDistributionThreshold(uint256 _proposalDistributionThreshold) private { + proposalDistributionThreshold = _proposalDistributionThreshold; + emit ProposalDistributionThresholdSet(_proposalDistributionThreshold); } /// @notice Private function to set a proposal's type data. diff --git a/packages/contracts-bedrock/src/governance/VotingModule.sol b/packages/contracts-bedrock/src/governance/VotingModule.sol deleted file mode 100644 index 02b692d87f1..00000000000 --- a/packages/contracts-bedrock/src/governance/VotingModule.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -abstract contract VotingModule { - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address immutable governor; - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotGovernor(); // nosemgrep: - error ExistingProposal(); // nosemgrep: - error InvalidParams(); // nosemgrep: - error AlreadyVoted(); // nosemgrep: - - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - - function _onlyGovernor() internal view { - if (msg.sender != governor) revert NotGovernor(); - } - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _governor) { - governor = _governor; - } - - /*////////////////////////////////////////////////////////////// - WRITE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - function propose(uint256 proposalId, bytes memory proposalData, bytes32 descriptionHash) external virtual; - - function _countVote( - uint256 proposalId, - address account, - uint8 support, - uint256 weight, - bytes memory params - ) - external - virtual; - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - function _formatExecuteParams( - uint256 proposalId, - bytes memory proposalData - ) - external - virtual - returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas); - - function _voteSucceeded(uint256 /* proposalId */ ) external view virtual returns (bool) { - return true; - } - - function COUNTING_MODE() external pure virtual returns (string memory); - - function PROPOSAL_DATA_ENCODING() external pure virtual returns (string memory); - - function VOTE_PARAMS_ENCODING() external pure virtual returns (string memory); -} diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 6971a1de14b..c41765cd037 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.15; // Interfaces import { IProposalValidator } from "interfaces/governance/IProposalValidator.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; -import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, @@ -16,6 +15,8 @@ import { import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; +import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; @@ -27,15 +28,6 @@ import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -// Modules -import { - ProposalSettings as ApprovalProposalSettings, - ProposalOption, - PassingCriteria -} from "src/governance/ApprovalVotingModule.sol"; -import { ProposalSettings as OptimisticProposalSettings } from "src/governance/OptimisticModule.sol"; -import { VotingModule } from "src/governance/VotingModule.sol"; - // Testing utilities import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; @@ -46,15 +38,9 @@ contract ProposalValidatorForTest is ProposalValidator { constructor( bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor, - IGovernanceToken _governanceToken + IOptimismGovernor _governor ) - ProposalValidator( - _approvedProposerAttestationSchemaUid, - _topDelegatesAttestationSchemaUid, - _governor, - _governanceToken - ) + ProposalValidator(_approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid, _governor) { } function hashProposalWithModule( @@ -157,7 +143,7 @@ contract ProposalValidator_Init is CommonTest { event VotingCycleDataSet( uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit ); - event DistributionThresholdSet(uint256 newDistributionThreshold); + event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule ); @@ -309,7 +295,8 @@ contract ProposalValidator_Init is CommonTest { returns (bytes memory) { // Construct ProposalOption array - ProposalOption[] memory options = new ProposalOption[](descriptions.length); + IApprovalVotingModule.ProposalOption[] memory options = + new IApprovalVotingModule.ProposalOption[](descriptions.length); for (uint256 i = 0; i < descriptions.length; i++) { address[] memory targets = new address[](1); @@ -319,7 +306,7 @@ contract ProposalValidator_Init is CommonTest { targets[0] = Predeploys.GOVERNANCE_TOKEN; calldatas[0] = abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i])); - options[i] = ProposalOption({ + options[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: amounts[i], targets: targets, values: values, @@ -335,9 +322,9 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), - criteria: uint8(PassingCriteria.Threshold), + criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: criteriaValue, budgetAmount: uint128(totalBudget) @@ -356,14 +343,15 @@ contract ProposalValidator_Init is CommonTest { returns (bytes memory) { // Construct ProposalOption array for elections (no execution calls) - ProposalOption[] memory options = new ProposalOption[](descriptions.length); + IApprovalVotingModule.ProposalOption[] memory options = + new IApprovalVotingModule.ProposalOption[](descriptions.length); for (uint256 i = 0; i < descriptions.length; i++) { address[] memory targets = new address[](0); uint256[] memory values = new uint256[](0); bytes[] memory calldatas = new bytes[](0); - options[i] = ProposalOption({ + options[i] = IApprovalVotingModule.ProposalOption({ budgetTokensSpent: 0, targets: targets, values: values, @@ -373,9 +361,9 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - ApprovalProposalSettings memory settings = ApprovalProposalSettings({ + IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), - criteria: uint8(PassingCriteria.TopChoices), + criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: criteriaValue, budgetAmount: 0 @@ -386,8 +374,8 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper function to construct voting module data for upgrade proposals function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { - OptimisticProposalSettings memory settings = - OptimisticProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); + IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); return abi.encode(settings); } @@ -502,7 +490,7 @@ contract ProposalValidator_Init is CommonTest { proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor ); validator = ProposalValidatorForTest(address(new Proxy(owner))); @@ -600,7 +588,7 @@ contract ProposalValidator_Init is CommonTest { contract ProposalValidator_Version_Test is ProposalValidator_Init { function test_version_succeeds() public view { string memory versionString = validator.version(); - assertEq(versionString, "1.0.0-beta.1"); + assertEq(versionString, "1.0.0"); } } @@ -613,7 +601,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor, governanceToken + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor ); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -644,7 +632,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { ); // Verify initialization was successful - assertEq(validator.distributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.proposalDistributionThreshold(), DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); // Verify voting cycle data @@ -768,7 +756,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -981,7 +969,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -1569,7 +1557,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); } - function testFuzz_submitFundingProposal_exceedsDistributionThreshold_reverts( + function testFuzz_submitFundingProposal_exceedsProposalDistributionThreshold_reverts( uint256 excessAmount, uint8 proposalTypeValue ) @@ -1967,7 +1955,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -2060,7 +2048,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(optimisticVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2099,7 +2087,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(expectedHash)) ); @@ -2206,7 +2194,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2278,7 +2266,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I abi.encodeCall( IOptimismGovernor.proposeWithModule, ( - VotingModule(approvalVotingModule), + approvalVotingModule, governanceFundVotingModuleData, governanceFundProposalDescription, uint8(governanceFundProposalType) @@ -2318,7 +2306,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I abi.encodeCall( IOptimismGovernor.proposeWithModule, ( - VotingModule(approvalVotingModule), + approvalVotingModule, councilBudgetVotingModuleData, councilBudgetProposalDescription, uint8(councilBudgetProposalType) @@ -2551,7 +2539,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } - function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsDistributionThreshold_reverts( + function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsProposalDistributionThreshold_reverts( uint8 _proposalTypeValue ) public @@ -2656,7 +2644,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (VotingModule(approvalVotingModule), votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) ), abi.encode(uint256(_randomHash)) ); @@ -2731,23 +2719,23 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { validator.setVotingCycleData(CYCLE_NUMBER, startingTimestamp, duration, distributionLimit); } - function testFuzz_setDistributionThreshold_succeeds(uint256 newDistributionThreshold) public { - // Expect the DistributionThresholdSet event to be emitted + function testFuzz_setProposalDistributionThreshold_succeeds(uint256 newProposalDistributionThreshold) public { + // Expect the ProposalDistributionThresholdSet event to be emitted vm.expectEmit(address(validator)); - emit DistributionThresholdSet(newDistributionThreshold); + emit ProposalDistributionThresholdSet(newProposalDistributionThreshold); vm.prank(owner); - validator.setDistributionThreshold(newDistributionThreshold); + validator.setProposalDistributionThreshold(newProposalDistributionThreshold); - assertEq(validator.distributionThreshold(), newDistributionThreshold); + assertEq(validator.proposalDistributionThreshold(), newProposalDistributionThreshold); } - function testFuzz_setDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { + function testFuzz_setProposalDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { vm.assume(caller != owner); vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setDistributionThreshold(threshold); + validator.setProposalDistributionThreshold(threshold); } function testFuzz_setProposalTypeData_succeeds( From a52974f2ff13ca6df1ddcaa19c453881cd0632f1 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 18 Jul 2025 05:57:03 -0300 Subject: [PATCH 55/73] fix: check hash after proposal (#447) * fix: add proposal id validation on submit upgrade proposal returned proposalId * fix: pre-pr --- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 7 +++- .../test/governance/ProposalValidator.t.sol | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 27c970a2404..44fca211286 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xe7a93826772bf108a21923f7e45b1f46cdadb75e48b0c796e43d64f0c1d81504", - "sourceCodeHash": "0xadd8e049bf3c652af123b6c64a1d504c92be13b4797ab10b978df907b05dcf7f" + "initCodeHash": "0x06a2b713907a4a4c961061edeb8f9417f9fdf63fc5512e6794af67fcc548935d", + "sourceCodeHash": "0x98cb001a29058d9d4bdd017c73e3520af1e22e9dee15e153799196b3390923df" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index d9ffaafe21b..f37a40d1a7f 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -369,10 +369,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { if (_proposalType == ProposalType.MaintenanceUpgrade) { proposal.movedToVote = true; - GOVERNOR.proposeWithModule( + uint256 proposalId = GOVERNOR.proposeWithModule( votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) ); + // Make sure the proposalId is the same as the proposalHash + if (proposalId != uint256(proposalHash_)) { + revert ProposalValidator_ProposalIdMismatch(); + } + emit ProposalMovedToVote(proposalHash_, msg.sender); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index c41765cd037..b538055e2c2 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1077,6 +1077,44 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); } + + function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 proposalId) public { + uint248 againstThreshold = 5000; + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Calculate expected proposal hash + bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes32 expectedHash = validator.hashProposalWithModule( + optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + ); + + vm.assume(proposalId != uint256(expectedHash)); // Ensure proposalId is different from expectedHash + + _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encode(0) + ); + + // Mock the proposeWithModule call to return a different proposalId + _mockAndExpect( + address(governor), + abi.encodeCall( + IOptimismGovernor.proposeWithModule, + (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + ), + abi.encode(proposalId) + ); + + vm.expectRevert(ProposalValidator.ProposalValidator_ProposalIdMismatch.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test From f53d1b3103ae0ea21395fe836994905d140be4ed Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:10:32 -0300 Subject: [PATCH 56/73] refactor: fetch proposal types configurator externally (#448) * refactor: fetch configurator from governor * fix: pre-pr * fix: pre-pr --- .../governance/IProposalValidator.sol | 4 --- .../snapshots/abi/ProposalValidator.json | 18 ----------- .../snapshots/semver-lock.json | 4 +-- .../storageLayout/ProposalValidator.json | 15 +++------- .../src/governance/ProposalValidator.sol | 30 +++++++++---------- .../test/governance/ProposalValidator.t.sol | 9 ++++-- 6 files changed, 26 insertions(+), 54 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 5c0c48a7be4..bf2c2e43778 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.0; // Interfaces import {IOptimismGovernor} from './IOptimismGovernor.sol'; import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IProposalTypesConfigurator } from './IProposalTypesConfigurator.sol'; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. @@ -167,7 +166,6 @@ interface IProposalValidator is ISemver { function initialize( address _owner, - IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, uint256 _startingTimestamp, uint256 _duration, @@ -195,8 +193,6 @@ interface IProposalValidator is ISemver { function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function proposalTypesConfigurator() external view returns (IProposalTypesConfigurator); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); function votingCycles(uint256) external view returns ( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 03a0733070c..9eddf4e0965 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -134,11 +134,6 @@ "name": "_owner", "type": "address" }, - { - "internalType": "contract IProposalTypesConfigurator", - "name": "_proposalTypesConfigurator", - "type": "address" - }, { "internalType": "uint256", "name": "_cycleNumber", @@ -315,19 +310,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "proposalTypesConfigurator", - "outputs": [ - { - "internalType": "contract IProposalTypesConfigurator", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 44fca211286..3ef81038526 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x06a2b713907a4a4c961061edeb8f9417f9fdf63fc5512e6794af67fcc548935d", - "sourceCodeHash": "0x98cb001a29058d9d4bdd017c73e3520af1e22e9dee15e153799196b3390923df" + "initCodeHash": "0xb612561f3182537796ec6bae6a15a4f6b9e63d839e051a165f6b81b3f6ce0807", + "sourceCodeHash": "0xbfb241a7033264d5b7097a4326a415b53514c4a4dee01430bd092fa6cbf0872b" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 2c01297b2b0..e8bbb446de6 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,39 +34,32 @@ "slot": "52", "type": "uint256[49]" }, - { - "bytes": "20", - "label": "proposalTypesConfigurator", - "offset": 0, - "slot": "101", - "type": "contract IProposalTypesConfigurator" - }, { "bytes": "32", "label": "proposalDistributionThreshold", "offset": 0, - "slot": "102", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "103", + "slot": "102", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "104", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "105", + "slot": "104", "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f37a40d1a7f..cf839cfe8b0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -217,9 +217,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The Optimism Governor contract that will handle the voting phase. IOptimismGovernor public immutable GOVERNOR; - /// @notice The proposal types configurator contract. - IProposalTypesConfigurator public proposalTypesConfigurator; - /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -258,7 +255,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. - /// @param _proposalTypesConfigurator The proposal types configurator contract address. /// @param _cycleNumber The number of the current voting cycle. /// @param _startingTimestamp The starting timestamp of the voting cycle. /// @param _duration The duration of the voting cycle. @@ -268,7 +264,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, - IProposalTypesConfigurator _proposalTypesConfigurator, uint256 _cycleNumber, uint256 _startingTimestamp, uint256 _duration, @@ -284,7 +279,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - proposalTypesConfigurator = _proposalTypesConfigurator; _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -338,8 +332,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Get the optimistic module address from configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = @@ -430,7 +425,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = proposalTypesConfigurator.proposalTypes( + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule ).module; @@ -517,8 +512,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); @@ -598,7 +594,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; - address votingModule = proposalTypesConfigurator.proposalTypes( + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule ).module; @@ -673,8 +669,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator ProposalType proposalType = ProposalType.CouncilMemberElections; - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = @@ -771,8 +768,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = - proposalTypesConfigurator.proposalTypes(proposalTypesData[_proposalType].proposalVotingModule).module; + address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( + proposalTypesData[_proposalType].proposalVotingModule + ).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b538055e2c2..bf90d08d5c6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -464,6 +464,12 @@ contract ProposalValidator_Init is CommonTest { moduleAddress = optimisticVotingModule; } + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.PROPOSAL_TYPES_CONFIGURATOR, ()), + abi.encode(proposalTypesConfigurator) + ); + _mockAndExpect( address(proposalTypesConfigurator), abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), @@ -501,7 +507,6 @@ contract ProposalValidator_Init is CommonTest { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, @@ -619,7 +624,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, @@ -691,7 +695,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { impl.initialize, ( owner, - proposalTypesConfigurator, CYCLE_NUMBER, START_TIMESTAMP, DURATION, From 056a7eccbf05214ec56ab5de2d13533971e0a72f Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Mon, 21 Jul 2025 06:08:59 -0300 Subject: [PATCH 57/73] fix: check for uninitialized voting modules (#446) * fix: check for uninitialized voting modules * fix: pre-pr * fix: pre-pr --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 39 +++++++--- .../test/governance/ProposalValidator.t.sol | 74 +++++++++++++++++++ 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index bf2c2e43778..b932d6e2461 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -29,6 +29,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_ProposalIdMismatch(); error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); + error ProposalValidator_InvalidVotingModule(); event ProposalSubmitted( bytes32 indexed proposalHash, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9eddf4e0965..7a1bb786918 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -853,6 +853,11 @@ "name": "ProposalValidator_InvalidVotingCycle", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidVotingModule", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 3ef81038526..f9c743116f7 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xb612561f3182537796ec6bae6a15a4f6b9e63d839e051a165f6b81b3f6ce0807", - "sourceCodeHash": "0xbfb241a7033264d5b7097a4326a415b53514c4a4dee01430bd092fa6cbf0872b" + "initCodeHash": "0xd072e288107128a1b0f26f41c4b257295919777cadfc06514d2a4738fb96e1d1", + "sourceCodeHash": "0x725efcbb68cb4a23af492983ba22969ddaa5d472878fb8490a437eaaa88812a8" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index cf839cfe8b0..f8f320e46bc 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -86,6 +86,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the proposal is invalid trying to move to vote. error ProposalValidator_InvalidProposal(); + /// @notice Thrown when the voting module address is invalid (zero address). + error ProposalValidator_InvalidVotingModule(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -332,9 +335,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Get the optimistic module address from configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = @@ -425,9 +434,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = @@ -512,9 +527,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( + GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() + ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + address votingModule = proposalTypeConfig.module; + + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index bf90d08d5c6..4bf2204b063 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -485,6 +485,36 @@ contract ProposalValidator_Init is CommonTest { ); } + /// @notice Helper function to mock proposal types configurator call with changed module + function _mockProposalTypesConfiguratorCallWithUninitializedModule(uint8 _votingModuleId) internal { + address moduleAddress; + if (_votingModuleId == APPROVAL_VOTING_MODULE_ID) { + moduleAddress = approvalVotingModule; + } else if (_votingModuleId == OPTIMISTIC_VOTING_MODULE_ID) { + moduleAddress = optimisticVotingModule; + } + + _mockAndExpect( + address(governor), + abi.encodeCall(IOptimismGovernor.PROPOSAL_TYPES_CONFIGURATOR, ()), + abi.encode(proposalTypesConfigurator) + ); + + _mockAndExpect( + address(proposalTypesConfigurator), + abi.encodeCall(IProposalTypesConfigurator.proposalTypes, (_votingModuleId)), + abi.encode( + IProposalTypesConfigurator.ProposalType({ + quorum: 0, + approvalThreshold: 0, + name: "", + description: "", + module: address(0) + }) + ) + ); + } + /// @notice Initializes the validator function _initializeValidator() internal virtual { ( @@ -943,6 +973,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function test_submitUpgradeProposal_invalidVotingModule_reverts() public { + uint248 againstThreshold = 5000; // 50% + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(OPTIMISTIC_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); @@ -1377,6 +1422,20 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER ); } + + function test_submitCouncilMemberElectionsProposal_invalidVotingModule_reverts() public { + attestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_SubmitFundingProposal_Test @@ -1742,6 +1801,21 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I CYCLE_NUMBER ); } + + function test_submitFundingProposal_invalidVotingModule_reverts() public { + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.GovernanceFund; + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + // Mock configurator to return uninitialized module + _mockProposalTypesConfiguratorCallWithUninitializedModule(APPROVAL_VOTING_MODULE_ID); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_ApproveProposal_Test From 61ed0e1cbf223f68e30fb5056db4b40fb53cdbe4 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:11:29 +0300 Subject: [PATCH 58/73] feat: add check in approve if proposal has moved to vote (#450) --- .../snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 5 +++++ .../test/governance/ProposalValidator.t.sol | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index f9c743116f7..a83197de31d 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd072e288107128a1b0f26f41c4b257295919777cadfc06514d2a4738fb96e1d1", - "sourceCodeHash": "0x725efcbb68cb4a23af492983ba22969ddaa5d472878fb8490a437eaaa88812a8" + "initCodeHash": "0x30d570ce61624852476452d9660567bae2346002878a45703cf60e44809730ca", + "sourceCodeHash": "0xe5272e8176b0ddf3ff589629f7872b5dc6c18b900f413f0f8682865bf310faa7" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index f8f320e46bc..6d2dc68fb9b 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -578,6 +578,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyApproved(); } + // check if proposal has already moved to vote + if (proposal.movedToVote) { + revert ProposalValidator_ProposalAlreadyMovedToVote(); + } + // validate the attestation _validateTopDelegateAttestation(_attestationUid, _msgSender()); diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 4bf2204b063..92f8863dfb5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1881,6 +1881,23 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalHash, topDelegateAttestation_A); } + function test_approveProposal_proposalAlreadyMovedToVote_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists and set movedToVote to true + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); + + vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); From 32c45b37b8bb3ea1e792f345af36697303aa3029 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:17:29 +0300 Subject: [PATCH 59/73] fix: msg sender to be consistent (#451) --- .../contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index a83197de31d..72bc5d7c7c5 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x30d570ce61624852476452d9660567bae2346002878a45703cf60e44809730ca", - "sourceCodeHash": "0xe5272e8176b0ddf3ff589629f7872b5dc6c18b900f413f0f8682865bf310faa7" + "initCodeHash": "0x0dfc44cf456909f524602c8d2b3e5793e04b59da994a8268bc59071aad567c77", + "sourceCodeHash": "0xbe31385272e8ecf4fd0bf52e768efcf9b6b4199f23707d74949c11ee6578e6de" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 6d2dc68fb9b..3a4659445f0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -362,11 +362,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, _proposalType); + emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) @@ -382,7 +382,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, msg.sender); + emit ProposalMovedToVote(proposalHash_, _msgSender()); } } @@ -461,11 +461,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = ProposalType.CouncilMemberElections; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } @@ -553,11 +553,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Store proposal metadata - proposal.proposer = msg.sender; + proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, msg.sender, _description, _proposalType); + emit ProposalSubmitted(proposalHash_, _msgSender(), _description, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); } @@ -915,7 +915,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { if ( attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID - || approvedDelegate != msg.sender || proposalType != uint8(_expectedProposalType) + || approvedDelegate != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } From 02f08220f7ae0df10d348fbe443a3143a4ac1013 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:35:37 +0300 Subject: [PATCH 60/73] fix: approved proposer schema (#452) --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 14 ++++-- .../test/governance/ProposalValidator.t.sol | 46 +++++++++++++++---- 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index b932d6e2461..7d93349984e 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -21,6 +21,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_ExceedsDistributionThreshold(); error ProposalValidator_InvalidOptionsLength(); error ProposalValidator_AttestationRevoked(); + error ProposalValidator_AttestationExpired(); error ProposalValidator_InvalidAttestationSchema(); error ProposalValidator_InvalidCriteriaValue(); error ProposalValidator_InvalidAgainstThreshold(); diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 7a1bb786918..2afaef89b24 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -788,6 +788,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationExpired", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_AttestationRevoked", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 72bc5d7c7c5..ed751c26bc0 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x0dfc44cf456909f524602c8d2b3e5793e04b59da994a8268bc59071aad567c77", - "sourceCodeHash": "0xbe31385272e8ecf4fd0bf52e768efcf9b6b4199f23707d74949c11ee6578e6de" + "initCodeHash": "0xbac284f6ec21a5d65d5b86d7e6406e0805d77e15dc4bd66397f0111701110e0e", + "sourceCodeHash": "0x58048692d1da18d17958b2dc950055ae2a58ab645385b0aed3528b289dfee21d" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 3a4659445f0..c871a2b9431 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -62,6 +62,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when an attestation is revoked. error ProposalValidator_AttestationRevoked(); + /// @notice Thrown when the attestation is expired. + error ProposalValidator_AttestationExpired(); + /// @notice Thrown when an attestation schema is invalid. error ProposalValidator_InvalidAttestationSchema(); @@ -210,7 +213,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. - /// @dev Schema format: { approvedProposer: address, proposalType: uint8 } + /// @dev Schema format: { proposalType: uint8, date: string } bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller @@ -911,11 +914,16 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } - (address approvedDelegate, uint8 proposalType) = abi.decode(attestation.data, (address, uint8)); + // check if the attestation is expired + if (attestation.expirationTime != 0 && attestation.expirationTime < block.timestamp) { + revert ProposalValidator_AttestationExpired(); + } + + (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID - || approvedDelegate != _msgSender() || proposalType != uint8(_expectedProposalType) + || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 92f8863dfb5..edab6a51f1a 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -115,6 +115,7 @@ contract ProposalValidator_Init is CommonTest { uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; + uint64 public constant ATT_EXPIRATION_TIME = 10 days; address owner; address user; @@ -561,7 +562,7 @@ contract ProposalValidator_Init is CommonTest { // Create schemas vm.prank(owner); APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = ISchemaRegistry(Predeploys.SCHEMA_REGISTRY).register( - "address approvedAddress,uint8 proposalType", ISchemaResolver(address(0)), true + "uint8 proposalType,string date", ISchemaResolver(address(0)), true ); vm.prank(owner); @@ -588,11 +589,11 @@ contract ProposalValidator_Init is CommonTest { AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: _delegate, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: true, refUID: bytes32(0), - data: abi.encode(_delegate, _proposalType), + data: abi.encode(_proposalType, "2000-01-01"), value: 0 }) }) @@ -948,6 +949,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 proposalTypeValue) public { + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + uint248 againstThreshold = 5000; + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + // warp the time to after the attestation expiration time + vm.warp(block.timestamp + ATT_EXPIRATION_TIME + 1); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + ); + } + function test_submitUpgradeProposal_zeroAgainstThreshold_reverts() public { uint248 zeroThreshold = 0; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; @@ -1083,11 +1099,11 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: topDelegate_A, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: false, refUID: bytes32(0), - data: abi.encode(topDelegate_A, proposalType), + data: abi.encode(proposalType, "2000-01-01"), value: 0 }) }) @@ -1292,6 +1308,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop ); } + function testFuzz_submitCouncilMemberElectionsProposal_attestationExpired_reverts() public { + // warp the time to after the attestation expiration time + vm.warp(block.timestamp + ATT_EXPIRATION_TIME + 1); + vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + ); + } + function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { string[] memory emptyOptions = new string[](0); @@ -1370,11 +1396,11 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop AttestationRequest({ schema: APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, data: AttestationRequestData({ - recipient: address(0), - expirationTime: 0, + recipient: topDelegate_A, + expirationTime: uint64(block.timestamp + ATT_EXPIRATION_TIME), revocable: false, refUID: bytes32(0), - data: abi.encode(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections), + data: abi.encode(ProposalValidator.ProposalType.CouncilMemberElections, "2000-01-01"), value: 0 }) }) From 2f0c65db71b1e14bd427c10a4c2045cdc41ad12d Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:04:42 +0300 Subject: [PATCH 61/73] fix: voting cycle validity on submit (#454) * fix: voting cycle validity on submit * fix: edge case on upgrade proposals --- .../governance/IProposalValidator.sol | 2 +- .../snapshots/abi/ProposalValidator.json | 2 +- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 25 +++++++- .../test/governance/ProposalValidator.t.sol | 59 +++++++++++++++---- 5 files changed, 74 insertions(+), 18 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 7d93349984e..f270847582d 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -107,7 +107,7 @@ interface IProposalValidator is ISemver { string memory _proposalDescription, bytes32 _attestationUid, ProposalType _proposalType, - uint256 _votingCycle + uint256 _latestVotingCycle ) external returns (bytes32 proposalHash_); function submitCouncilMemberElectionsProposal( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 2afaef89b24..044e2188e95 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -524,7 +524,7 @@ }, { "internalType": "uint256", - "name": "_votingCycle", + "name": "_latestVotingCycle", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index ed751c26bc0..2ad18f4000f 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xbac284f6ec21a5d65d5b86d7e6406e0805d77e15dc4bd66397f0111701110e0e", - "sourceCodeHash": "0x58048692d1da18d17958b2dc950055ae2a58ab645385b0aed3528b289dfee21d" + "initCodeHash": "0x33e740162106c353e5d9f550c67d47286846b80a91f042f5b600703e4ed0b42c", + "sourceCodeHash": "0xb51010203b3a894896a1e70c999f858d6d8e937c08feb301642e93f9787081e5" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index c871a2b9431..704bfd2653f 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -302,14 +302,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). - /// @param _votingCycle The voting cycle number the proposal is targetted for. + /// @param _latestVotingCycle The latest voting cycle number. Even though the upgrade proposal can be submitted + /// outside of a voting cycle, we still need the latest voting cycle number to validate top delegates attestations. /// @return proposalHash_ The hash of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, bytes32 _attestationUid, ProposalType _proposalType, - uint256 _votingCycle + uint256 _latestVotingCycle ) external returns (bytes32 proposalHash_) @@ -320,6 +321,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidUpgradeProposalType(); } + // Validate voting cycle exists + VotingCycleData memory latestVotingCycleData = votingCycles[_latestVotingCycle]; + if (latestVotingCycleData.startingTimestamp == 0) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate EAS attestation - must be called by owner-approved address _validateApprovedProposerAttestation(_attestationUid, _proposalType); @@ -367,7 +374,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Store proposal metadata proposal.proposer = _msgSender(); proposal.proposalType = _proposalType; - proposal.votingCycle = _votingCycle; + proposal.votingCycle = _latestVotingCycle; emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); @@ -407,6 +414,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (bytes32 proposalHash_) { + // Validate voting cycle exists and is not in the past + VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; + if (votingCycleData.startingTimestamp == 0 || votingCycleData.startingTimestamp < block.timestamp) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate EAS attestation - must be called by owner-approved address _validateApprovedProposerAttestation(_attestationUid, ProposalType.CouncilMemberElections); @@ -503,6 +516,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidFundingProposalType(); } + // Validate voting cycle exists and is not in the past + VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; + if (votingCycleData.startingTimestamp == 0 || votingCycleData.startingTimestamp < block.timestamp) { + revert ProposalValidator_InvalidVotingCycle(); + } + // Validate input arrays have matching lengths uint256 optionsLength = _optionsDescriptions.length; if (optionsLength != _optionsRecipients.length || optionsLength != _optionsAmounts.length) { diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index edab6a51f1a..b70fb6e43a7 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -895,6 +895,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init /// @notice Sad path tests for submitUpgradeProposal function contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { string proposalDescription; + uint248 againstThreshold = 5000; // 50% function setUp() public override { super.setUp(); @@ -910,7 +911,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; // 50% bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); @@ -920,8 +920,25 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitUpgradeProposal_invalidVotingCycle_reverts( + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); + vm.assume(votingCycle != CYCLE_NUMBER); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.submitUpgradeProposal( + againstThreshold, proposalDescription, attestationUid, proposalType, votingCycle + ); + } + function testFuzz_submitUpgradeProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 validAttestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -937,7 +954,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_unattestedProposer_reverts(address fuzzedProposer) public { vm.assume(fuzzedProposer != topDelegate_A); // Ensure it's different from attested proposer - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -952,7 +968,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 proposalTypeValue) public { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // warp the time to after the attestation expiration time @@ -990,7 +1005,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I } function test_submitUpgradeProposal_invalidVotingModule_reverts() public { - uint248 againstThreshold = 5000; // 50% ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1009,7 +1023,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal hash @@ -1061,7 +1074,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal hash @@ -1090,7 +1102,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I function testFuzz_submitUpgradeProposal_attestationNotFromOwner_reverts(address fuzzedAttester) public { vm.assume(fuzzedAttester != owner); // Ensure it's not the approved owner - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; // Create attestation but don't use proper owner as attester @@ -1121,8 +1132,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - uint248 againstThreshold = 5000; - // Create valid attestation first (make it revocable) bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1143,7 +1152,6 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I } function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 proposalId) public { - uint248 againstThreshold = 5000; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1285,6 +1293,15 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } + function testFuzz_submitCouncilMemberElectionsProposal_invalidVotingCycle_reverts(uint256 votingCycle) public { + vm.assume(votingCycle != CYCLE_NUMBER); + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.submitCouncilMemberElectionsProposal( + criteriaValue, optionDescriptions, proposalDescription, attestationUid, votingCycle + ); + } + function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { @@ -1581,6 +1598,26 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I ); } + function testFuzz_submitFundingProposal_invalidVotingCycle_reverts( + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + vm.assume(votingCycle != CYCLE_NUMBER); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, votingCycle + ); + } + function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( uint8 matchingLength, uint8 mismatchedLength, From 006e9acffff30c5f2814191bbfe3e56aa35a12cc Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 22 Jul 2025 22:14:08 +0300 Subject: [PATCH 62/73] fix: budget cap dos (#453) * fix: budget cap dos * fix: invalid proposal case * fix: test * fix: tests --- .../governance/IProposalValidator.sol | 3 +- .../snapshots/abi/ProposalValidator.json | 10 +++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 52 +++++++++++-- .../test/governance/ProposalValidator.t.sol | 76 +++++++++++++++++-- 5 files changed, 132 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index f270847582d..2682c9eb5a7 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -31,6 +31,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); error ProposalValidator_InvalidVotingModule(); + error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( bytes32 indexed proposalHash, @@ -130,7 +131,7 @@ interface IProposalValidator is ISemver { function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_); + function canApproveProposal(bytes32 _attestationUid, address _delegate, bytes32 _proposalHash) external view returns (bool canApprove_); function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 044e2188e95..9c41904e7fd 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -101,6 +101,11 @@ "internalType": "address", "name": "_delegate", "type": "address" + }, + { + "internalType": "bytes32", + "name": "_proposalHash", + "type": "bytes32" } ], "name": "canApproveProposal", @@ -788,6 +793,11 @@ "name": "VotingCycleDataSet", "type": "event" }, + { + "inputs": [], + "name": "ProposalValidator_AttestationCreatedAfterLastVotingCycle", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_AttestationExpired", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2ad18f4000f..2c83371d4a5 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x33e740162106c353e5d9f550c67d47286846b80a91f042f5b600703e4ed0b42c", - "sourceCodeHash": "0xb51010203b3a894896a1e70c999f858d6d8e937c08feb301642e93f9787081e5" + "initCodeHash": "0xd359b54afbf8f0bfcde0e24b648d20f1eecdc306b511d3c0cd90afa5b61382ac", + "sourceCodeHash": "0x6da6042e1bc89da33dad2ce39aaef685f2a67c04e4693f69c2693b349899d131" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 704bfd2653f..537d808e314 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -92,6 +92,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the voting module address is invalid (zero address). error ProposalValidator_InvalidVotingModule(); + /// @notice Thrown when the attestation was created after the last voting cycle. + error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -591,7 +594,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address _delegate = _msgSender(); ProposalData storage proposal = _proposals[_proposalHash]; // check if the proposal exists - if (proposal.proposer == address(0)) { + // proposal.votingCycle should never be 0, voting cycles already exist before the ProposalValidator is deployed + // and should be set by the OP Foundation + if (proposal.proposer == address(0) || proposal.votingCycle == 0) { revert ProposalValidator_ProposalDoesNotExist(); } @@ -605,8 +610,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyMovedToVote(); } + // The previous voting cycle of a proposal should be the one before the + // proposal's targetted voting cycle. + uint256 previousVotingCycle = proposal.votingCycle - 1; + // Proposal or Governor Upgrade proposals are submitted with the latest voting cycle number, + // because they can be submitted outside of a voting cycle. + if (proposal.proposalType == ProposalType.ProtocolOrGovernorUpgrade) { + previousVotingCycle = proposal.votingCycle; + } + // validate the attestation - _validateTopDelegateAttestation(_attestationUid, _msgSender()); + _validateTopDelegateAttestation(_attestationUid, _msgSender(), previousVotingCycle); // store the approval proposal.delegateApprovals[_delegate] = true; @@ -618,9 +632,25 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Checks if a delegate can approve a proposal. /// @dev Helper function for UI integration. /// @param _attestationUid The UID of the attestation to check. + /// @param _delegate The delegate to check the attestation for. + /// @param _proposalHash The hash of the proposal to check the attestation for. /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal(bytes32 _attestationUid, address _delegate) external view returns (bool canApprove_) { - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate); + function canApproveProposal( + bytes32 _attestationUid, + address _delegate, + bytes32 _proposalHash + ) + external + view + returns (bool canApprove_) + { + // TODO: this function should be fixed in OPT-957 + ProposalData storage proposal = _proposals[_proposalHash]; + if (proposal.votingCycle == 0) { + return false; + } + + canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate, proposal.votingCycle - 1); } /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. @@ -955,13 +985,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @return canApprove_ True if the attestation is valid, false otherwise. function _validateTopDelegateAttestation( bytes32 _attestationUid, - address _delegate + address _delegate, + uint256 _lastVotingCycle ) internal view returns (bool canApprove_) { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); + VotingCycleData memory previousVotingCycleData = votingCycles[_lastVotingCycle]; + if (previousVotingCycleData.startingTimestamp == 0) { + revert ProposalValidator_InvalidVotingCycle(); + } // Check if attestation exists, equivalent to calling EAS.isAttestationValid(_attestationUid) if (attestation.uid == bytes32(0)) { @@ -978,6 +1013,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_AttestationRevoked(); } + // since the attestations are updated daily we should only allow attestations + // created before the last voting cycle of the proposal + // check if attestation was created after the previous voting cycle + if (attestation.time > previousVotingCycleData.startingTimestamp + previousVotingCycleData.duration) { + revert ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + } + (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); // check if the attestation includes partial delegation or the recipient is not the caller diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index b70fb6e43a7..7e88af159b6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1884,6 +1884,15 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { + function setUp() public override { + super.setUp(); + + // create a new voting cycle + // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + } + function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Ensure the proposal hash is not 0 vm.assume(_proposalHash != bytes32(0)); @@ -1961,6 +1970,25 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalHash, topDelegateAttestation_A); } + function test_approveProposal_invalidVotingCycle_reverts( + bytes32 _proposalHash, + uint8 proposalTypeValue, + uint256 votingCycle + ) + public + { + vm.assume(votingCycle != CYCLE_NUMBER); + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, votingCycle); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalHash, topDelegateAttestation_A); + } + function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); @@ -1990,6 +2018,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // set proposal data so that the proposal exists validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); @@ -2002,6 +2033,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // revoke the attestation vm.prank(owner); @@ -2033,6 +2067,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -2052,6 +2089,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // create an attestation with partial delegation vm.prank(owner); @@ -2091,6 +2131,9 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + // set the voting cycle data of the previous cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -2102,20 +2145,34 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { /// @title ProposalValidator_CanApproveProposal_Test /// @notice Tests for the canApproveProposal function contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds() public view { + function test_canApproveProposal_returnTrue_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set the voting cycle data of the previous cycle + validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A); + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); assertTrue(canApprove); } - function test_canApproveProposal_returnFalse_succeeds(bytes32 attestationUid, address delegate) public { + function test_canApproveProposal_returnFalseRevert_succeeds( + bytes32 _attestationUid, + address _delegate, + bytes32 _proposalHash + ) + public + { // Ensure the attestation uid is not one of the top delegates - vm.assume(attestationUid != topDelegateAttestation_A); + vm.assume(_attestationUid != topDelegateAttestation_A); bool canApprove; // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(attestationUid, delegate) returns (bool result_) { + try validator.canApproveProposal(_attestationUid, _delegate, _proposalHash) returns (bool result_) { canApprove = result_; } catch { canApprove = false; @@ -2123,6 +2180,15 @@ contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { assertEq(canApprove, false); } + + function test_canApproveProposal_returnFalseProposalNotFound_reverts(bytes32 _proposalHash) public { + validator.setProposalData( + _proposalHash, topDelegate_A, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, 0, 0 + ); + + bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); + assertEq(canApprove, false); + } } /// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test From 98143cc5f1c24e8a95972cde7164541d9d7b1644 Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:33:05 -0300 Subject: [PATCH 63/73] fix: move to vote logic (#455) * refactor: improve variable naming * fix: wrong arg sent to external proposeWithModuole calls * fix: pre-pr * fix: stack too deep error * fix: pre-pr * fix: pre-pr --------- Signed-off-by: Chiin <77933451+0xChin@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 6 +- .../snapshots/abi/ProposalValidator.json | 8 +- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 100 ++++++++++-------- .../test/governance/ProposalValidator.t.sol | 76 ++++++------- 5 files changed, 97 insertions(+), 97 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 2682c9eb5a7..090499667d3 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -62,7 +62,7 @@ interface IProposalValidator is ISemver { event ProposalTypeDataSet( ProposalType proposalType, uint256 requiredApprovals, - uint8 proposalVotingModule + uint8 idInConfigurator ); event ProposalVotingModuleData( @@ -85,7 +85,7 @@ interface IProposalValidator is ISemver { struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalVotingModule; + uint8 idInConfigurator; } struct VotingCycleData { @@ -196,7 +196,7 @@ interface IProposalValidator is ISemver { function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); - function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 proposalVotingModule); + function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 idInConfigurator); function votingCycles(uint256) external view returns ( uint256 startingTimestamp, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 9c41904e7fd..66b2363eda4 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -178,7 +178,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -332,7 +332,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -375,7 +375,7 @@ }, { "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], @@ -736,7 +736,7 @@ { "indexed": false, "internalType": "uint8", - "name": "proposalVotingModule", + "name": "idInConfigurator", "type": "uint8" } ], diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2c83371d4a5..37bb34e9489 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd359b54afbf8f0bfcde0e24b648d20f1eecdc306b511d3c0cd90afa5b61382ac", - "sourceCodeHash": "0x6da6042e1bc89da33dad2ce39aaef685f2a67c04e4693f69c2693b349899d131" + "initCodeHash": "0xca95fa18ecb8d7d44043dd60c545719e526fdf487ea405c8321d25b92782bfd6", + "sourceCodeHash": "0xf0b71a440952d0a4a00a488f5b98da2010d1972297876ea8ade494d0885f7508" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 537d808e314..42242128349 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -134,8 +134,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param proposalVotingModule The proposal type ID. - event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule); + /// @param idInConfigurator The proposal type ID. + event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. /// @param proposalHash The hash of the submitted proposal. @@ -165,10 +165,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Struct for storing explicit data for each proposal type. /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. - /// @param proposalVotingModule The proposal type ID used to get the voting module from the configurator. + /// @param idInConfigurator The proposal type ID used to get the voting module from the configurator. struct ProposalTypeData { uint256 requiredApprovals; - uint8 proposalVotingModule; + uint8 idInConfigurator; } /// @notice Struct for storing voting cycle data. @@ -325,8 +325,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Validate voting cycle exists - VotingCycleData memory latestVotingCycleData = votingCycles[_latestVotingCycle]; - if (latestVotingCycleData.startingTimestamp == 0) { + if (votingCycles[_latestVotingCycle].startingTimestamp == 0) { revert ProposalValidator_InvalidVotingCycle(); } @@ -338,24 +337,29 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidAgainstThreshold(); } - // Create OptimisticModule ProposalSettings with required parameters - IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ - againstThreshold: _againstThreshold, - isRelativeToVotableSupply: true // MUST always be true - }); - // Optimistic proposals are signal-only, no execution targets/calldatas needed - bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); + bytes memory proposalVotingModuleData = abi.encode( + IOptimisticModule.ProposalSettings({ + againstThreshold: _againstThreshold, + isRelativeToVotableSupply: true // MUST always be true + }) + ); + + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; // Get the optimistic module address from configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); - address votingModule = proposalTypeConfig.module; + address votingModule; + { + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); - // Validate voting module exists - if (bytes(proposalTypeConfig.name).length == 0) { - revert ProposalValidator_InvalidVotingModule(); + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } + + votingModule = proposalTypeConfig.module; } // Generate unique proposal hash @@ -387,7 +391,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(_proposalType) + votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator ); // Make sure the proposalId is the same as the proposalHash @@ -455,7 +459,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].proposalVotingModule); + ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator); address votingModule = proposalTypeConfig.module; // Validate voting module exists @@ -554,7 +558,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].proposalVotingModule); + ).proposalTypes(proposalTypesData[_proposalType].idInConfigurator); address votingModule = proposalTypeConfig.module; // Validate voting module exists @@ -670,11 +674,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(settings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].idInConfigurator; + // Get the module address from the configurator ProposalType proposalType = ProposalType.ProtocolOrGovernorUpgrade; - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = @@ -705,9 +711,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -745,11 +750,14 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); + ProposalType _proposalType = ProposalType.CouncilMemberElections; + + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - ProposalType proposalType = ProposalType.CouncilMemberElections; - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[proposalType].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = @@ -758,7 +766,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ProposalData storage proposal = _proposals[proposalHash_]; // Proposal must exist and be valid - if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { + if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { revert ProposalValidator_InvalidProposal(); } @@ -768,7 +776,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Check if proposal has enough approvals - if (proposal.approvalCount < proposalTypesData[proposalType].requiredApprovals) { + if (proposal.approvalCount < proposalTypesData[_proposalType].requiredApprovals) { revert ProposalValidator_InsufficientApprovals(); } @@ -789,9 +797,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, uint8(proposalType) - ); + uint256 proposalId = + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -824,7 +831,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (bytes32 proposalHash_) { - uint256 optionsLength = _optionsDescriptions.length; // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { revert ProposalValidator_InvalidFundingProposalType(); @@ -836,7 +842,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Configure approval module settings IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(optionsLength), + maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, criteriaValue: _criteriaValue, @@ -845,10 +851,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, settings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes( - proposalTypesData[_proposalType].proposalVotingModule - ).module; + address votingModule = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; // Generate unique proposal hash proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); @@ -890,7 +898,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Propose with module on the Governor uint256 proposalId = - GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, uint8(_proposalType)); + GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, idInConfigurator); // Make sure the proposalId is the same as the proposalHash if (proposalId != uint256(proposalHash_)) { @@ -1143,8 +1151,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalTypeData The data for the proposal type. function _setProposalTypeData(ProposalType _proposalType, ProposalTypeData memory _proposalTypeData) private { proposalTypesData[_proposalType] = _proposalTypeData; - emit ProposalTypeDataSet( - _proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.proposalVotingModule - ); + emit ProposalTypeDataSet(_proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.idInConfigurator); } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7e88af159b6..15eea020bd5 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -146,7 +146,7 @@ contract ProposalValidator_Init is CommonTest { ); event ProposalDistributionThresholdSet(uint256 newProposalDistributionThreshold); event ProposalTypeDataSet( - ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 proposalVotingModule + ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator ); event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); @@ -167,9 +167,9 @@ contract ProposalValidator_Init is CommonTest { stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(0) .checked_write(_data.requiredApprovals); - // Set proposalVotingModule (depth 1) + // Set idInConfigurator (depth 1) stdstore.target(address(validator)).sig("proposalTypesData(uint8)").with_key(uint256(_proposalType)).depth(1) - .checked_write(_data.proposalVotingModule); + .checked_write(_data.idInConfigurator); } /// @notice Helper function to set CouncilMemberElections proposal type data. @@ -178,7 +178,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilMemberElections, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -189,7 +189,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.GovernanceFund, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -200,7 +200,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.CouncilBudget, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }) ); } @@ -211,7 +211,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }) ); } @@ -222,7 +222,7 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType.MaintenanceUpgrade, ProposalValidator.ProposalTypeData({ requiredApprovals: 0, // MaintenanceUpgrade moves directly to voting - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }) ); } @@ -259,27 +259,25 @@ contract ProposalValidator_Init is CommonTest { // ProtocolOrGovernorUpgrade proposalTypesData[0] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID + idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }); // MaintenanceUpgrade - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: 0, - proposalVotingModule: OPTIMISTIC_VOTING_MODULE_ID - }); + proposalTypesData[1] = + ProposalValidator.ProposalTypeData({ requiredApprovals: 0, idInConfigurator: OPTIMISTIC_VOTING_MODULE_ID }); // CouncilMemberElections proposalTypesData[2] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); // GovernanceFund proposalTypesData[3] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); // CouncilBudget proposalTypesData[4] = ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: APPROVAL_VOTING_MODULE_ID + idInConfigurator: APPROVAL_VOTING_MODULE_ID }); return (proposalTypes, proposalTypesData); @@ -680,7 +678,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalTypes[i]); + (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalTypes[i]); if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { assertEq(requiredApprovals, 0); } else { @@ -693,10 +691,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections ) { - assertEq(proposalVotingModule, APPROVAL_VOTING_MODULE_ID); + assertEq(idInConfigurator, APPROVAL_VOTING_MODULE_ID); } else { // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID - assertEq(proposalVotingModule, OPTIMISTIC_VOTING_MODULE_ID); + assertEq(idInConfigurator, OPTIMISTIC_VOTING_MODULE_ID); } } } @@ -709,14 +707,10 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mismatched array with different length ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); - proposalTypesData[0] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 0 - }); - proposalTypesData[1] = ProposalValidator.ProposalTypeData({ - requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, - proposalVotingModule: 1 - }); + proposalTypesData[0] = + ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 0 }); + proposalTypesData[1] = + ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 1 }); vm.prank(owner); vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); @@ -790,7 +784,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -1046,7 +1040,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -1176,7 +1170,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(proposalId) ); @@ -2216,7 +2210,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -2309,7 +2303,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -2348,7 +2342,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(expectedHash)) ); @@ -2455,7 +2449,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -2530,7 +2524,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I approvalVotingModule, governanceFundVotingModuleData, governanceFundProposalDescription, - uint8(governanceFundProposalType) + APPROVAL_VOTING_MODULE_ID ) ), abi.encode(uint256(expectedGovernanceFundHash)) @@ -2570,7 +2564,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I approvalVotingModule, councilBudgetVotingModuleData, councilBudgetProposalDescription, - uint8(councilBudgetProposalType) + APPROVAL_VOTING_MODULE_ID ) ), abi.encode(uint256(expectedCouncilBudgetHash)) @@ -2905,7 +2899,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, uint8(proposalType)) + (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), abi.encode(uint256(_randomHash)) ); @@ -3012,7 +3006,7 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: newRequiredApprovals, - proposalVotingModule: newProposalTypeId + idInConfigurator: newProposalTypeId }); // Expect the ProposalTypeDataSet event to be emitted @@ -3022,16 +3016,16 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { vm.prank(owner); validator.setProposalTypeData(proposalType, newData); - (uint256 requiredApprovals, uint8 proposalVotingModule) = validator.proposalTypesData(proposalType); + (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalType); assertEq(requiredApprovals, newRequiredApprovals); - assertEq(proposalVotingModule, newProposalTypeId); + assertEq(idInConfigurator, newProposalTypeId); } function testFuzz_setProposalTypeData_notOwner_reverts(address caller) public { vm.assume(caller != owner); ProposalValidator.ProposalTypeData memory newData = - ProposalValidator.ProposalTypeData({ requiredApprovals: 4, proposalVotingModule: 0 }); + ProposalValidator.ProposalTypeData({ requiredApprovals: 4, idInConfigurator: 0 }); vm.prank(caller); vm.expectRevert("Ownable: caller is not the owner"); From 161ce4f844485bfe2662a7ebe98522b3d8887f2d Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:54:55 -0300 Subject: [PATCH 64/73] fix: normalize validate functions (#456) * refactor: remove canApproveProposal and normalize validate functions * fix(test): handle invalid voting cycle edge case * fix: pre-pr --- .../governance/IProposalValidator.sol | 2 - .../snapshots/abi/ProposalValidator.json | 29 ----------- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 43 ++-------------- .../test/governance/ProposalValidator.t.sol | 51 +------------------ 5 files changed, 7 insertions(+), 122 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 090499667d3..ec63c98eaff 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -131,8 +131,6 @@ interface IProposalValidator is ISemver { function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; - function canApproveProposal(bytes32 _attestationUid, address _delegate, bytes32 _proposalHash) external view returns (bool canApprove_); - function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 66b2363eda4..dc516cfa49b 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -90,35 +90,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_attestationUid", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "_delegate", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "_proposalHash", - "type": "bytes32" - } - ], - "name": "canApproveProposal", - "outputs": [ - { - "internalType": "bool", - "name": "canApprove_", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 37bb34e9489..70279a91241 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xca95fa18ecb8d7d44043dd60c545719e526fdf487ea405c8321d25b92782bfd6", - "sourceCodeHash": "0xf0b71a440952d0a4a00a488f5b98da2010d1972297876ea8ade494d0885f7508" + "initCodeHash": "0x6bcced3e1050048ecc63fd4d9a86ec72e756de4cb328d29c357cc31c87834341", + "sourceCodeHash": "0x9380a09eeb5f12304baf0d45ea14e7bbd1b046d805429bfe0de970a7dfccd176" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 42242128349..30702c95efa 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -624,7 +624,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // validate the attestation - _validateTopDelegateAttestation(_attestationUid, _msgSender(), previousVotingCycle); + _validateTopDelegateAttestation(_attestationUid, previousVotingCycle); // store the approval proposal.delegateApprovals[_delegate] = true; @@ -633,30 +633,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { emit ProposalApproved(_proposalHash, _delegate); } - /// @notice Checks if a delegate can approve a proposal. - /// @dev Helper function for UI integration. - /// @param _attestationUid The UID of the attestation to check. - /// @param _delegate The delegate to check the attestation for. - /// @param _proposalHash The hash of the proposal to check the attestation for. - /// @return canApprove_ True if the delegate can approve the proposal, false otherwise. - function canApproveProposal( - bytes32 _attestationUid, - address _delegate, - bytes32 _proposalHash - ) - external - view - returns (bool canApprove_) - { - // TODO: this function should be fixed in OPT-957 - ProposalData storage proposal = _proposals[_proposalHash]; - if (proposal.votingCycle == 0) { - return false; - } - - canApprove_ = _validateTopDelegateAttestation(_attestationUid, _delegate, proposal.votingCycle - 1); - } - /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. /// @param _againstThreshold The threshold for the proposal to be against the total supply. /// @param _proposalDescription Description of the proposal. @@ -989,17 +965,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Validates the attestation data for a delegate that tries to approve a proposal. /// @dev Only acceptes attestations that does NOT include partial delegation. /// @param _attestationUid The UID of the attestation to validate. - /// @param _delegate The delegate to validate the attestation for. - /// @return canApprove_ True if the attestation is valid, false otherwise. - function _validateTopDelegateAttestation( - bytes32 _attestationUid, - address _delegate, - uint256 _lastVotingCycle - ) - internal - view - returns (bool canApprove_) - { + /// @param _lastVotingCycle The last voting cycle to validate against. + function _validateTopDelegateAttestation(bytes32 _attestationUid, uint256 _lastVotingCycle) internal view { Attestation memory attestation = IEAS(Predeploys.EAS).getAttestation(_attestationUid); VotingCycleData memory previousVotingCycleData = votingCycles[_lastVotingCycle]; if (previousVotingCycleData.startingTimestamp == 0) { @@ -1031,11 +998,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (, bool _includePartialDelegation,) = abi.decode(attestation.data, (string, bool, string)); // check if the attestation includes partial delegation or the recipient is not the caller - if (_includePartialDelegation || attestation.recipient != _delegate) { + if (_includePartialDelegation || attestation.recipient != _msgSender()) { revert ProposalValidator_InvalidAttestation(); } - - canApprove_ = true; } /// @notice Internal function to build proposal options with optional execution data. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 15eea020bd5..1407a1dc601 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1971,7 +1971,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ) public { - vm.assume(votingCycle != CYCLE_NUMBER); + vm.assume(votingCycle != CYCLE_NUMBER && votingCycle != 0); // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -2136,55 +2136,6 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { } } -/// @title ProposalValidator_CanApproveProposal_Test -/// @notice Tests for the canApproveProposal function -contract ProposalValidator_CanApproveProposal_Test is ProposalValidator_Init { - function test_canApproveProposal_returnTrue_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - // set the voting cycle data of the previous cycle - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); - - // Attestation already created in setUp - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); - assertTrue(canApprove); - } - - function test_canApproveProposal_returnFalseRevert_succeeds( - bytes32 _attestationUid, - address _delegate, - bytes32 _proposalHash - ) - public - { - // Ensure the attestation uid is not one of the top delegates - vm.assume(_attestationUid != topDelegateAttestation_A); - - bool canApprove; - // Expect the invalid attestation error to be reverted - vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - try validator.canApproveProposal(_attestationUid, _delegate, _proposalHash) returns (bool result_) { - canApprove = result_; - } catch { - canApprove = false; - } - - assertEq(canApprove, false); - } - - function test_canApproveProposal_returnFalseProposalNotFound_reverts(bytes32 _proposalHash) public { - validator.setProposalData( - _proposalHash, topDelegate_A, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, 0, 0 - ); - - bool canApprove = validator.canApproveProposal(topDelegateAttestation_A, topDelegate_A, _proposalHash); - assertEq(canApprove, false); - } -} - /// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test /// @notice Happy path tests for moveToVoteProtocolOrGovernorUpgradeProposal function contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { From 1a442a193d217cfa6c6c3d798b127ca550ab64ee Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:07:02 -0300 Subject: [PATCH 65/73] chore: use fixed variable for contract version (#457) --- .../snapshots/abi/ProposalValidator.json | 2 +- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../src/governance/ProposalValidator.sol | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index dc516cfa49b..c1f63a7b24d 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -538,7 +538,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 70279a91241..ac84dff8c87 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x6bcced3e1050048ecc63fd4d9a86ec72e756de4cb328d29c357cc31c87834341", - "sourceCodeHash": "0x9380a09eeb5f12304baf0d45ea14e7bbd1b046d805429bfe0de970a7dfccd176" + "initCodeHash": "0x8f58c80a20e9f5e631d9451d86b2b5478a60879e58420f6788e75629f5863430", + "sourceCodeHash": "0x1d4ce0d6f868bd36418e7e760c81ecb033c15720e6a35f088ea135620fcae91a" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 30702c95efa..19500f835f0 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -206,6 +206,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { CONSTANTS //////////////////////////////////////////////////////////////*/ + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + /// @notice The divisor used for percentage calculations in optimistic voting modules. /// @dev Represents 100% in basis points (10,000 = 100%). uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; @@ -238,12 +242,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of proposal hash to their corresponding proposal data. mapping(bytes32 => ProposalData) internal _proposals; - /// @notice Semantic version. - /// @custom:semver 1.0.0 - function version() public pure virtual returns (string memory) { - return "1.0.0"; - } - /// @notice Constructs the ProposalValidator contract. /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller From df382abe56cc6d122da503069b063b0ea4fea60f Mon Sep 17 00:00:00 2001 From: Chiin <77933451+0xChin@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:57:29 -0300 Subject: [PATCH 66/73] fix: pp minors (#458) * chore: use fixed variable for contract version * refactor: rename proposalHash to proposalId for governor consistency * chore: move attestation schemas uids to initialize function * chore: specify target modules in submit functions * refactor: proper naming for modules settings * fix: pre-pr --- .../governance/IProposalValidator.sol | 30 +- .../snapshots/abi/ProposalValidator.json | 134 +++--- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 24 +- .../src/governance/ProposalValidator.sol | 202 +++++---- .../test/governance/ProposalValidator.t.sol | 413 +++++++++--------- 6 files changed, 399 insertions(+), 408 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index ec63c98eaff..291ba4833b0 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -34,19 +34,19 @@ interface IProposalValidator is ISemver { error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed proposer, string description, ProposalType proposalType ); event ProposalApproved( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed approver ); event ProposalMovedToVote( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed executor ); @@ -66,7 +66,7 @@ interface IProposalValidator is ISemver { ); event ProposalVotingModuleData( - bytes32 indexed proposalHash, + uint256 indexed proposalId, bytes encodedVotingModuleData ); @@ -109,7 +109,7 @@ interface IProposalValidator is ISemver { bytes32 _attestationUid, ProposalType _proposalType, uint256 _latestVotingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, @@ -117,7 +117,7 @@ interface IProposalValidator is ISemver { string memory _proposalDescription, bytes32 _attestationUid, uint256 _votingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function submitFundingProposal( uint128 _criteriaValue, @@ -127,20 +127,20 @@ interface IProposalValidator is ISemver { string memory _description, ProposalType _proposalType, uint256 _votingCycle - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external; + function approveProposal(uint256 _proposalId, bytes32 _attestationUid) external; function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function moveToVoteCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, string memory _proposalDescription - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function moveToVoteFundingProposal( uint128 _criteriaValue, @@ -149,7 +149,7 @@ interface IProposalValidator is ISemver { uint256[] memory _optionsAmounts, string memory _description, ProposalType _proposalType - ) external returns (bytes32 proposalHash_); + ) external returns (uint256 proposalId_); function setVotingCycleData( uint256 _cycleNumber, @@ -172,6 +172,8 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -188,9 +190,9 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function approvedProposerAttestationSchemaUid() external view returns (bytes32); - function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); + function topDelegatesAttestationSchemaUid() external view returns (bytes32); function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); @@ -204,8 +206,6 @@ interface IProposalValidator is ISemver { ); function __constructor__( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, IOptimismGovernor _governor ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index c1f63a7b24d..b26b2647557 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -1,16 +1,6 @@ [ { "inputs": [ - { - "internalType": "bytes32", - "name": "_approvedProposerAttestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "_topDelegatesAttestationSchemaUid", - "type": "bytes32" - }, { "internalType": "contract IOptimismGovernor", "name": "_governor", @@ -20,19 +10,6 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "GOVERNOR", @@ -60,34 +37,34 @@ "type": "function" }, { - "inputs": [], - "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", - "outputs": [ + "inputs": [ + { + "internalType": "uint256", + "name": "_proposalId", + "type": "uint256" + }, { "internalType": "bytes32", - "name": "", + "name": "_attestationUid", "type": "bytes32" } ], - "stateMutability": "view", + "name": "approveProposal", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [ - { - "internalType": "bytes32", - "name": "_proposalHash", - "type": "bytes32" - }, + "inputs": [], + "name": "approvedProposerAttestationSchemaUid", + "outputs": [ { "internalType": "bytes32", - "name": "_attestationUid", + "name": "", "type": "bytes32" } ], - "name": "approveProposal", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -135,6 +112,16 @@ "name": "_proposalDistributionThreshold", "type": "uint256" }, + { + "internalType": "bytes32", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", + "type": "bytes32" + }, { "internalType": "enum ProposalValidator.ProposalType[]", "name": "_proposalTypes", @@ -184,9 +171,9 @@ "name": "moveToVoteCouncilMemberElectionsProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -228,9 +215,9 @@ "name": "moveToVoteFundingProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -252,9 +239,9 @@ "name": "moveToVoteProtocolOrGovernorUpgradeProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -419,9 +406,9 @@ "name": "submitCouncilMemberElectionsProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -468,9 +455,9 @@ "name": "submitFundingProposal", "outputs": [ { - "internalType": "bytes32", - "name": "proposalHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -505,14 +492,27 @@ } ], "name": "submitUpgradeProposal", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "topDelegatesAttestationSchemaUid", "outputs": [ { "internalType": "bytes32", - "name": "proposalHash_", + "name": "", "type": "bytes32" } ], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -612,9 +612,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -644,9 +644,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -663,9 +663,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": true, @@ -719,9 +719,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "proposalHash", - "type": "bytes32" + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" }, { "indexed": false, diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index ac84dff8c87..9f4dc118d5c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x8f58c80a20e9f5e631d9451d86b2b5478a60879e58420f6788e75629f5863430", - "sourceCodeHash": "0x1d4ce0d6f868bd36418e7e760c81ecb033c15720e6a35f088ea135620fcae91a" + "initCodeHash": "0x4171ca5a66e60a2a3715dc3108d76f985eb16937d71197c74ee0465461e8f9fe", + "sourceCodeHash": "0xa95d144dad3bb25ef0ac0e4ae9039b1ee5304e86e9ef7c1726d263f63598f864" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index e8bbb446de6..2b847ae3993 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -36,30 +36,44 @@ }, { "bytes": "32", - "label": "proposalDistributionThreshold", + "label": "approvedProposerAttestationSchemaUid", "offset": 0, "slot": "101", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "topDelegatesAttestationSchemaUid", + "offset": 0, + "slot": "102", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "proposalDistributionThreshold", + "offset": 0, + "slot": "103", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "102", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "103", + "slot": "105", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "104", - "type": "mapping(bytes32 => struct ProposalValidator.ProposalData)" + "slot": "106", + "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 19500f835f0..61db34720bb 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -80,7 +80,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the trying to move a proposal to vote outside of the accepted voting cycle. error ProposalValidator_InvalidVotingCycle(); - /// @notice Thrown when the proposalId returned by the Governor is not the same as the proposalHash. + /// @notice Thrown when the proposalId returned by the Governor does not match the expected proposalId. error ProposalValidator_ProposalIdMismatch(); /// @notice Thrown when the caller is not the proposer. @@ -100,23 +100,23 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a new proposal is submitted. - /// @param proposalHash The hash of the submitted proposal. + /// @param proposalId The ID of the submitted proposal. /// @param proposer The address that submitted the proposal. /// @param description Description of the proposal. /// @param proposalType Type of the proposal. event ProposalSubmitted( - bytes32 indexed proposalHash, address indexed proposer, string description, ProposalType proposalType + uint256 indexed proposalId, address indexed proposer, string description, ProposalType proposalType ); /// @notice Emitted when a delegate approves a proposal. - /// @param proposalHash The hash of the approved proposal. + /// @param proposalId The ID of the approved proposal. /// @param approver The address of the delegate who approved the proposal. - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); + event ProposalApproved(uint256 indexed proposalId, address indexed approver); /// @notice Emitted when a proposal is moved to the voting phase in the governor contract. - /// @param proposalHash The hash of the proposal moved to vote. + /// @param proposalId The ID of the proposal moved to vote. /// @param executor The address that executed the move to vote. - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); /// @notice Emitted when the voting cycle data is set. /// @param cycleNumber The number of the voting cycle. @@ -138,9 +138,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. - /// @param proposalHash The hash of the submitted proposal. + /// @param proposalId The ID of the submitted proposal. /// @param encodedVotingModuleData The encoded voting module data. - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + event ProposalVotingModuleData(uint256 indexed proposalId, bytes encodedVotingModuleData); /*////////////////////////////////////////////////////////////// STRUCTS @@ -166,6 +166,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param requiredApprovals The number of approvals each proposal type requires in order to be able to move for /// voting. /// @param idInConfigurator The proposal type ID used to get the voting module from the configurator. + /// @dev Based on the spec document, funding and council member elections proposals are + /// configured for the ApprovalVotingModule, while the upgrade proposals are configured for the + /// OptimisticVotingModule. + /// Any change on the module used for proposals would require the Validator to be upgraded. struct ProposalTypeData { uint256 requiredApprovals; uint8 idInConfigurator; @@ -218,17 +222,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { STATE VARIABLES //////////////////////////////////////////////////////////////*/ + /// @notice The Optimism Governor contract that will handle the voting phase. + IOptimismGovernor public immutable GOVERNOR; + /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { proposalType: uint8, date: string } - bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public approvedProposerAttestationSchemaUid; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is part of the top100 delegates. - bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; - - /// @notice The Optimism Governor contract that will handle the voting phase. - IOptimismGovernor public immutable GOVERNOR; + bytes32 public topDelegatesAttestationSchemaUid; /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -239,23 +243,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Mapping of proposal types to their corresponding data. mapping(ProposalType => ProposalTypeData) public proposalTypesData; - /// @notice Mapping of proposal hash to their corresponding proposal data. - mapping(bytes32 => ProposalData) internal _proposals; + /// @notice Mapping of proposal ID to their corresponding proposal data. + mapping(uint256 => ProposalData) internal _proposals; /// @notice Constructs the ProposalValidator contract. - /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in EAS for submitting proposals. - /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in EAS for checking if the caller - /// is part of the top100 delegates. /// @param _governor The Optimism Governor contract address. - constructor( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor - ) - ReinitializableBase(1) - { - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; - TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; + constructor(IOptimismGovernor _governor) ReinitializableBase(1) { GOVERNOR = _governor; _disableInitializers(); } @@ -276,6 +269,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -286,6 +281,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } + approvedProposerAttestationSchemaUid = _approvedProposerAttestationSchemaUid; + topDelegatesAttestationSchemaUid = _topDelegatesAttestationSchemaUid; + _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -305,7 +303,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalType The type of proposal (ProtocolOrGovernorUpgrade or MaintenanceUpgrade). /// @param _latestVotingCycle The latest voting cycle number. Even though the upgrade proposal can be submitted /// outside of a voting cycle, we still need the latest voting cycle number to validate top delegates attestations. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription, @@ -314,7 +312,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _latestVotingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Validate proposal type is valid for upgrade proposals if (_proposalType != ProposalType.ProtocolOrGovernorUpgrade && _proposalType != ProposalType.MaintenanceUpgrade) @@ -360,11 +358,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingModule = proposalTypeConfig.module; } - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Prevent duplicate proposals if (proposal.proposer != address(0)) { @@ -372,7 +370,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -381,8 +379,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = _proposalType; proposal.votingCycle = _latestVotingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, _proposalType); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _proposalDescription, _proposalType); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); // MaintenanceUpgrade proposals move directly to voting (atomic operation) if (_proposalType == ProposalType.MaintenanceUpgrade) { @@ -392,12 +390,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator ); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } } @@ -408,7 +406,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _proposalDescription Description of the proposal. /// @param _attestationUid The UID of the attestation for the approved proposer. /// @param _votingCycle The voting cycle number the proposal is targetted for. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionDescriptions, @@ -417,7 +415,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Validate voting cycle exists and is not in the past VotingCycleData memory votingCycleData = votingCycles[_votingCycle]; @@ -444,7 +442,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionDescriptions, new address[](0), new uint256[](0)); // Configure approval voting settings with TopChoices criteria - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections @@ -452,7 +450,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: 0 // No budget amount for elections }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( @@ -465,19 +463,19 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidVotingModule(); } - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; - // Prevent duplicate proposals with same hash + // Prevent duplicate proposals with same ID if (proposal.proposer != address(0)) { revert ProposalValidator_ProposalAlreadySubmitted(); } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -486,8 +484,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = ProposalType.CouncilMemberElections; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _proposalDescription, ProposalType.CouncilMemberElections); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); } /// @notice Submits a GovernanceFund or CouncilBudget proposal type that transfers OP tokens for approval and @@ -503,7 +501,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). /// @param _votingCycle The voting cycle number the proposal is targetted for. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function submitFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, @@ -514,7 +512,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _votingCycle ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { @@ -543,7 +541,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval voting settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -551,7 +549,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: uint128(totalBudget) }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Get the module address from the configurator IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( @@ -564,18 +562,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidVotingModule(); } - // Generate unique proposal hash - proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; - // Prevent duplicate proposals with same hash + // Prevent duplicate proposals with same ID if (proposal.proposer != address(0)) { revert ProposalValidator_ProposalAlreadySubmitted(); } // Check if proposal already exists in OptimismGovernor - if (GOVERNOR.proposalSnapshot(uint256(proposalHash_)) != 0) { + if (GOVERNOR.proposalSnapshot(proposalId_) != 0) { revert ProposalValidator_ProposalAlreadySubmitted(); } @@ -584,17 +582,17 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.proposalType = _proposalType; proposal.votingCycle = _votingCycle; - emit ProposalSubmitted(proposalHash_, _msgSender(), _description, _proposalType); - emit ProposalVotingModuleData(proposalHash_, proposalVotingModuleData); + emit ProposalSubmitted(proposalId_, _msgSender(), _description, _proposalType); + emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData); } /// @notice Approves a proposal before being moved for voting. /// @dev This function should only be called by the top delegates. - /// @param _proposalHash The hash of the proposal to approve + /// @param _proposalId The ID of the proposal to approve /// @param _attestationUid The UID of the attestation for the delegate to approve the proposal - function approveProposal(bytes32 _proposalHash, bytes32 _attestationUid) external { + function approveProposal(uint256 _proposalId, bytes32 _attestationUid) external { address _delegate = _msgSender(); - ProposalData storage proposal = _proposals[_proposalHash]; + ProposalData storage proposal = _proposals[_proposalId]; // check if the proposal exists // proposal.votingCycle should never be 0, voting cycles already exist before the ProposalValidator is deployed // and should be set by the OP Foundation @@ -628,25 +626,25 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.delegateApprovals[_delegate] = true; proposal.approvalCount++; - emit ProposalApproved(_proposalHash, _delegate); + emit ProposalApproved(_proposalId, _delegate); } /// @notice Moves a Protocol or Governor Upgrade proposal to vote by proposing it on the Governor. /// @param _againstThreshold The threshold for the proposal to be against the total supply. /// @param _proposalDescription Description of the proposal. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteProtocolOrGovernorUpgradeProposal( uint248 _againstThreshold, string memory _proposalDescription ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Configure optimistic proposal settings - IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); - bytes memory proposalVotingModuleData = abi.encode(settings); + bytes memory proposalVotingModuleData = abi.encode(optimisticSettings); // Retrieve the ID to use in the proposal type configurator uint8 idInConfigurator = proposalTypesData[ProposalType.ProtocolOrGovernorUpgrade].idInConfigurator; @@ -656,11 +654,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist and be valid if (proposal.proposer == address(0) || proposal.proposalType != proposalType) { @@ -688,33 +686,33 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Moves a council member elections proposal to vote by proposing it on the Governor. /// @param _criteriaValue The number of top choices that can pass the voting. /// @param _optionsDescriptions The strings of the different options that can be voted. /// @param _proposalDescription Description of the proposal. - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteCouncilMemberElectionsProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, string memory _proposalDescription ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), @@ -722,7 +720,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: 0 }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); ProposalType _proposalType = ProposalType.CouncilMemberElections; @@ -733,11 +731,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist and be valid if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { @@ -774,12 +772,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Moves a funding proposal to vote by proposing it on the Governor. @@ -793,7 +791,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @param _optionsAmounts The amount to transfer for each option in case the option passes the voting. /// @param _description Description of the proposal. /// @param _proposalType The type of proposal (must be GovernanceFund or CouncilBudget). - /// @return proposalHash_ The hash of the submitted proposal. + /// @return proposalId_ The ID of the submitted proposal. function moveToVoteFundingProposal( uint128 _criteriaValue, string[] memory _optionsDescriptions, @@ -803,7 +801,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ProposalType _proposalType ) external - returns (bytes32 proposalHash_) + returns (uint256 proposalId_) { // Only funding proposal types can use this function if (_proposalType != ProposalType.GovernanceFund && _proposalType != ProposalType.CouncilBudget) { @@ -815,7 +813,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); // Configure approval module settings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -823,7 +821,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { budgetAmount: uint128(totalBudget) }); - bytes memory proposalVotingModuleData = abi.encode(options, settings); + bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); // Retrieve the ID to use in the proposal type configurator uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; @@ -832,10 +830,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { address votingModule = IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator).module; - // Generate unique proposal hash - proposalHash_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); + // Generate unique proposal ID + proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_description))); - ProposalData storage proposal = _proposals[proposalHash_]; + ProposalData storage proposal = _proposals[proposalId_]; // Proposal must exist if (proposal.proposer == address(0) || proposal.proposalType != _proposalType) { @@ -874,12 +872,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 proposalId = GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, idInConfigurator); - // Make sure the proposalId is the same as the proposalHash - if (proposalId != uint256(proposalHash_)) { + // Make sure the proposalId matches + if (proposalId != proposalId_) { revert ProposalValidator_ProposalIdMismatch(); } - emit ProposalMovedToVote(proposalHash_, _msgSender()); + emit ProposalMovedToVote(proposalId_, _msgSender()); } /// @notice Sets the data of a voting cycle. @@ -953,7 +951,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( - attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID + attestation.attester != owner() || attestation.schema != approvedProposerAttestationSchemaUid || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); @@ -961,7 +959,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } /// @notice Validates the attestation data for a delegate that tries to approve a proposal. - /// @dev Only acceptes attestations that does NOT include partial delegation. + /// @dev Only accepts attestations that do NOT include partial delegation. /// @param _attestationUid The UID of the attestation to validate. /// @param _lastVotingCycle The last voting cycle to validate against. function _validateTopDelegateAttestation(bytes32 _attestationUid, uint256 _lastVotingCycle) internal view { @@ -977,7 +975,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // check if the schema is correct - if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { + if (attestation.schema != topDelegatesAttestationSchemaUid) { revert ProposalValidator_InvalidAttestationSchema(); } @@ -1057,11 +1055,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } } - /// @notice Calculate `proposalId` hashing similarly to `hashProposal` but based on `module` and `proposalData`. + /// @notice Calculate `proposalId` based on `module`, `proposalData` and `descriptionHash`. /// @param _module The address of the voting module to use for this proposal. /// @param _proposalData The proposal data to pass to the voting module. /// @param _descriptionHash The hash of the proposal description. - /// @return The hash of the proposal. + /// @return The proposal ID as uint256. function _hashProposalWithModule( address _module, bytes memory _proposalData, @@ -1069,9 +1067,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { ) internal view - returns (bytes32) + returns (uint256) { - return keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash)); + return uint256(keccak256(abi.encode(address(GOVERNOR), _module, _proposalData, _descriptionHash))); } /// @notice Private function to set the voting cycle data and emit event. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 1407a1dc601..a307afbb918 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -33,15 +33,9 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest -/// @notice A test contract that exposes the private _hashProposal function +/// @notice A test contract that exposes the private _hashProposalWithModule function contract ProposalValidatorForTest is ProposalValidator { - constructor( - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, - IOptimismGovernor _governor - ) - ProposalValidator(_approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid, _governor) - { } + constructor(IOptimismGovernor _governor) ProposalValidator(_governor) { } function hashProposalWithModule( address _module, @@ -50,13 +44,13 @@ contract ProposalValidatorForTest is ProposalValidator { ) public view - returns (bytes32) + returns (uint256) { return _hashProposalWithModule(_module, _proposalData, _descriptionHash); } /// @notice Exposes proposal data for testing - function getProposalData(bytes32 _proposalHash) + function getProposalData(uint256 _proposalId) public view returns ( @@ -67,14 +61,14 @@ contract ProposalValidatorForTest is ProposalValidator { uint256 votingCycle_ ) { - ProposalData storage proposal = _proposals[_proposalHash]; + ProposalData storage proposal = _proposals[_proposalId]; return ( proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle ); } function setProposalData( - bytes32 _proposalHash, + uint256 _proposalId, address _proposer, ProposalType _proposalType, bool _movedToVote, @@ -83,20 +77,20 @@ contract ProposalValidatorForTest is ProposalValidator { ) public { - _proposals[_proposalHash].proposer = _proposer; - _proposals[_proposalHash].proposalType = _proposalType; - _proposals[_proposalHash].movedToVote = _movedToVote; - _proposals[_proposalHash].approvalCount = _approvalCount; - _proposals[_proposalHash].votingCycle = _votingCycle; + _proposals[_proposalId].proposer = _proposer; + _proposals[_proposalId].proposalType = _proposalType; + _proposals[_proposalId].movedToVote = _movedToVote; + _proposals[_proposalId].approvalCount = _approvalCount; + _proposals[_proposalId].votingCycle = _votingCycle; } - function mockApproveProposal(bytes32 _proposalHash, address _delegate) public { - _proposals[_proposalHash].delegateApprovals[_delegate] = true; + function mockApproveProposal(uint256 _proposalId, address _delegate) public { + _proposals[_proposalId].delegateApprovals[_delegate] = true; } /// @notice Check if a delegate has approved a proposal - function hasDelegateApproved(bytes32 _proposalHash, address _delegate) public view returns (bool hasApproved_) { - return _proposals[_proposalHash].delegateApprovals[_delegate]; + function hasDelegateApproved(uint256 _proposalId, address _delegate) public view returns (bool hasApproved_) { + return _proposals[_proposalId].delegateApprovals[_delegate]; } } @@ -116,6 +110,8 @@ contract ProposalValidator_Init is CommonTest { uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; uint64 public constant ATT_EXPIRATION_TIME = 10 days; + bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; + bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; address owner; address user; @@ -129,17 +125,15 @@ contract ProposalValidator_Init is CommonTest { ProposalValidatorForTest public impl; IOptimismGovernor public governor; IProposalTypesConfigurator public proposalTypesConfigurator; - bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; - bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; event ProposalSubmitted( - bytes32 indexed proposalHash, + uint256 indexed proposalId, address indexed proposer, string description, ProposalValidator.ProposalType proposalType ); - event ProposalApproved(bytes32 indexed proposalHash, address indexed approver); - event ProposalMovedToVote(bytes32 indexed proposalHash, address indexed executor); + event ProposalApproved(uint256 indexed proposalId, address indexed approver); + event ProposalMovedToVote(uint256 indexed proposalId, address indexed executor); event MinimumVotingPowerSet(uint256 newMinimumVotingPower); event VotingCycleDataSet( uint256 cycleNumber, uint256 startingTimestamp, uint256 duration, uint256 votingCycleDistributionLimit @@ -148,7 +142,7 @@ contract ProposalValidator_Init is CommonTest { event ProposalTypeDataSet( ProposalValidator.ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator ); - event ProposalVotingModuleData(bytes32 indexed proposalHash, bytes encodedVotingModuleData); + event ProposalVotingModuleData(uint256 indexed proposalId, bytes encodedVotingModuleData); /// @notice Helper function to setup a mock and expect a call to it. function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { @@ -321,7 +315,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, @@ -329,7 +323,7 @@ contract ProposalValidator_Init is CommonTest { budgetAmount: uint128(totalBudget) }); - return abi.encode(options, settings); + return abi.encode(options, approvalSettings); } /// @notice Helper function to construct voting module data for council elections @@ -360,7 +354,7 @@ contract ProposalValidator_Init is CommonTest { } // Construct ProposalSettings with TopChoices criteria - IApprovalVotingModule.ProposalSettings memory settings = IApprovalVotingModule.ProposalSettings({ + IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ maxApprovals: uint8(descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), @@ -368,15 +362,15 @@ contract ProposalValidator_Init is CommonTest { budgetAmount: 0 }); - return abi.encode(options, settings); + return abi.encode(options, approvalSettings); } /// @notice Helper function to construct voting module data for upgrade proposals function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { - IOptimisticModule.ProposalSettings memory settings = + IOptimisticModule.ProposalSettings memory optimisticSettings = IOptimisticModule.ProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); - return abi.encode(settings); + return abi.encode(optimisticSettings); } /// @notice Helper function to create a proposal for move to vote @@ -386,17 +380,17 @@ contract ProposalValidator_Init is CommonTest { string memory proposalDescription ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { - // Calculate expected proposal hash + // Calculate expected proposal ID votingModuleData_ = _constructOptimisticVotingModuleData(againstThreshold); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); // 1 vote as default for being able to move to vote validator.setProposalData( - proposalHash_, + proposalId_, proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, @@ -413,15 +407,15 @@ contract ProposalValidator_Init is CommonTest { string memory proposalDescription ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { votingModuleData_ = _constructCouncilElectionVotingModuleData(optionsDescriptions, criteriaValue); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); validator.setProposalData( - proposalHash_, + proposalId_, proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, @@ -441,17 +435,15 @@ contract ProposalValidator_Init is CommonTest { ProposalValidator.ProposalType proposalType ) internal - returns (bytes32 proposalHash_, bytes memory votingModuleData_) + returns (uint256 proposalId_, bytes memory votingModuleData_) { votingModuleData_ = _constructFundingVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); - proposalHash_ = validator.hashProposalWithModule( + proposalId_ = validator.hashProposalWithModule( approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) ); - validator.setProposalData( - proposalHash_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER - ); + validator.setProposalData(proposalId_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); } /// @notice Helper function to setup proposal types configurator mocks @@ -524,9 +516,7 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor - ); + impl = new ProposalValidatorForTest(governor); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -541,6 +531,8 @@ contract ProposalValidator_Init is CommonTest { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -634,9 +626,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest( - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID, governor - ); + impl = new ProposalValidatorForTest(governor); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -658,6 +648,8 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -725,6 +717,8 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, + TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -764,17 +758,15 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init // Create attestation for the proposal bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -786,25 +778,25 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, proposer); + emit ProposalMovedToVote(expectedId, proposer); vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( + uint256 proposalId = validator.submitUpgradeProposal( againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -813,7 +805,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -839,34 +831,32 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init // Create attestation for the proposal bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); vm.prank(proposer); - bytes32 proposalHash = validator.submitUpgradeProposal( + uint256 proposalId = validator.submitUpgradeProposal( againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -875,7 +865,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -1019,17 +1009,15 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -1042,7 +1030,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); } @@ -1070,16 +1058,16 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1149,20 +1137,18 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); - vm.assume(proposalId != uint256(expectedHash)); // Ensure proposalId is different from expectedHash + vm.assume(proposalId != expectedId); // Ensure proposalId is different from expectedId _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Mock the proposeWithModule call to return a different proposalId @@ -1210,37 +1196,35 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Expect ProposalSubmitted event vm.expectEmit(address(validator)); emit ProposalSubmitted( - expectedHash, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections + expectedId, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections ); // Expect ProposalVotingModuleData event vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(topDelegate_A); - bytes32 proposalHash = validator.submitCouncilMemberElectionsProposal( + uint256 proposalId = validator.submitCouncilMemberElectionsProposal( criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -1249,7 +1233,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(proposer, topDelegate_A, "Proposer should be topDelegate_A"); assertEq( @@ -1340,17 +1324,15 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop } function test_submitCouncilMemberElectionsProposal_duplicateProposal_reverts() public { - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -1373,16 +1355,16 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop } function test_submitCouncilMemberElectionsProposal_proposalExistsInGovernor_reverts() public { - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); - bytes32 expectedHash = validator.hashProposalWithModule( + uint256 expectedId = validator.hashProposalWithModule( approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1517,35 +1499,33 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init amounts[i] = amount; } - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, criteriaValue); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); // Expect ProposalSubmitted event vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedHash, proposer, description, proposalType); + emit ProposalSubmitted(expectedId, proposer, description, proposalType); // Expect ProposalVotingModuleData event vm.expectEmit(address(validator)); - emit ProposalVotingModuleData(expectedHash, votingModuleData); + emit ProposalVotingModuleData(expectedId, votingModuleData); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.prank(proposer); - bytes32 proposalHash = validator.submitFundingProposal( + uint256 proposalId = validator.submitFundingProposal( criteriaValue, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); - assertEq(proposalHash, expectedHash); + assertEq(proposalId, expectedId); // Verify proposal data was stored correctly ( @@ -1554,7 +1534,7 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init bool movedToVote, uint256 approvalCount, uint256 votingCycle - ) = validator.getProposalData(proposalHash); + ) = validator.getProposalData(proposalId); assertEq(storedProposer, proposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); @@ -1747,17 +1727,15 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( - address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), - abi.encode(0) + address(governor), abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(0) ); _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -1787,16 +1765,16 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); - // Calculate expected proposal hash + // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - bytes32 expectedHash = + uint256 expectedId = validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( address(governor), - abi.encodeCall(IOptimismGovernor.proposalSnapshot, (uint256(expectedHash))), + abi.encodeCall(IOptimismGovernor.proposalSnapshot, (expectedId)), abi.encode(1000) // Non-zero indicates proposal exists ); @@ -1887,29 +1865,29 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); } - function test_approveProposal_succeeds(bytes32 _proposalHash, uint8 proposalTypeValue) public { - // Ensure the proposal hash is not 0 - vm.assume(_proposalHash != bytes32(0)); + function test_approveProposal_succeeds(uint256 _proposalId, uint8 proposalTypeValue) public { + // Ensure the proposal ID is not 0 + vm.assume(_proposalId != 0); // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect event to be emitted when approving vm.expectEmit(address(validator)); - emit ProposalApproved(_proposalHash, topDelegate_A); + emit ProposalApproved(_proposalId, topDelegate_A); // Approve the proposal, use the attestation of the top delegate that was created in setUp vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); // Check that the proposal data has been updated - assertTrue(validator.hasDelegateApproved(_proposalHash, topDelegate_A)); + assertTrue(validator.hasDelegateApproved(_proposalId, topDelegate_A)); - (,,, uint256 approvalCount,) = validator.getProposalData(_proposalHash); + (,,, uint256 approvalCount,) = validator.getProposalData(_proposalId); assertEq(approvalCount, 1); } } @@ -1921,15 +1899,15 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { super.setUp(); } - function test_approveProposal_proposalDoesNotExist_reverts(bytes32 _proposalHash) public { + function test_approveProposal_proposalDoesNotExist_reverts(uint256 _proposalId) public { // There is no stored proposal data so this will revert vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyApproved_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -1938,17 +1916,17 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal as already approved by the top delegate - validator.mockApproveProposal(_proposalHash, topDelegate_A); + validator.mockApproveProposal(_proposalId, topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyMovedToVote_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -1957,33 +1935,36 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists and set movedToVote to true - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidVotingCycle_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, uint256 votingCycle ) public { vm.assume(votingCycle != CYCLE_NUMBER && votingCycle != 0); + vm.assume(votingCycle != CYCLE_NUMBER + 1); // Avoid existing cycle + // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, votingCycle); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, votingCycle); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } - function test_approveProposal_invalidSchema_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + function test_approveProposal_invalidSchema_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); @@ -2011,22 +1992,22 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _invalidAttestationUid); + validator.approveProposal(_proposalId, _invalidAttestationUid); } - function test_approveProposal_attestationRevoked_reverts(bytes32 _proposalHash, uint8 proposalTypeValue) public { + function test_approveProposal_attestationRevoked_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { // Bound the proposal type to valid enum values (0-4) proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2042,11 +2023,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidAttestationCaller_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, address _caller ) @@ -2060,7 +2041,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.assume(_caller != topDelegate_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2068,11 +2049,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(_caller); - validator.approveProposal(_proposalHash, topDelegateAttestation_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); } function test_approveProposal_invalidAttestationPartialDelegation_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue ) public @@ -2082,7 +2063,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2106,11 +2087,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _attestationUidWithPartialDelegation); + validator.approveProposal(_proposalId, _attestationUidWithPartialDelegation); } function test_approveProposal_nonExistentAttestation_reverts( - bytes32 _proposalHash, + uint256 _proposalId, uint8 proposalTypeValue, bytes32 _nonExistentAttestationUid ) @@ -2124,7 +2105,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.assume(_nonExistentAttestationUid != topDelegateAttestation_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalHash, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // set the voting cycle data of the previous cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); @@ -2132,7 +2113,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalHash, _nonExistentAttestationUid); + validator.approveProposal(_proposalId, _nonExistentAttestationUid); } } @@ -2143,12 +2124,12 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P string proposalDescription = "Test proposal"; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes votingModuleData; - bytes32 expectedHash; + uint256 expectedId; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = + (expectedId, votingModuleData) = _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); } @@ -2163,19 +2144,19 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalMovedToVote(expectedId, approvedProposer); // Move to vote vm.prank(approvedProposer); validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } } @@ -2185,12 +2166,12 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail string proposalDescription = "Test proposal"; ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes votingModuleData; - bytes32 expectedHash; + uint256 expectedId; function setUp() public override { super.setUp(); - (expectedHash, votingModuleData) = + (expectedId, votingModuleData) = _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); } @@ -2208,7 +2189,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 _againstThreshold) public { - // This will generate a different proposal hash which will make the proposal type wrong + // This will generate a different proposal ID which will make the proposal type wrong vm.assume(_againstThreshold != againstThreshold); // Mock the proposal types configurator call @@ -2221,7 +2202,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -2236,15 +2217,15 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(approvedProposer); validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(uint256 _randomId) public { + vm.assume(_randomId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -2256,7 +2237,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail IOptimismGovernor.proposeWithModule, (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2268,7 +2249,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; uint128 criteriaValue = 1; - bytes32 expectedHash; + uint256 expectedId; bytes votingModuleData; string proposalDescription = "Test proposal"; string[] optionsDescriptions = new string[](2); @@ -2279,7 +2260,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop // Create a proposal for move to vote with 1 top choice and 2 options optionsDescriptions[0] = "Option 1"; optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); } @@ -2295,12 +2276,12 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(expectedHash)) + abi.encode(expectedId) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedHash, approvedProposer); + emit ProposalMovedToVote(expectedId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2308,7 +2289,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } } @@ -2318,7 +2299,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is uint128 criteriaValue = 1; string proposalDescription = "Test proposal"; string[] optionsDescriptions = new string[](2); - bytes32 expectedHash; + uint256 expectedId; bytes votingModuleData; function setUp() public override { @@ -2327,7 +2308,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is // Create a proposal for move to vote with 1 top choice and 2 options optionsDescriptions[0] = "Option 1"; optionsDescriptions[1] = "Option 2"; - (expectedHash, votingModuleData) = _createCouncilElectionProposalForMoveToVote( + (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, proposalDescription ); } @@ -2344,7 +2325,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is } function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { - // This will generate a different proposal hash which will make the proposal type wrong + // This will generate a different proposal ID which will make the proposal type wrong uint128 _criteriaValue = 2; // we use 2 since it is the max based on the created proposal in setUp // Mock the proposal types configurator call @@ -2357,7 +2338,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(expectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2369,7 +2350,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { // Set proposal data movedToVote to true - validator.setProposalData(expectedHash, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); + validator.setProposalData(expectedId, approvedProposer, proposalType, true, 2, CYCLE_NUMBER); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2389,8 +2370,8 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); } - function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(bytes32 _randomHash) public { - vm.assume(_randomHash != expectedHash); + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(uint256 _randomId) public { + vm.assume(_randomId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2402,7 +2383,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2421,8 +2402,8 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I string[] optionsDescriptions = new string[](2); address[] optionsRecipients = new address[](2); uint256[] optionsAmounts = new uint256[](2); - bytes32 expectedGovernanceFundHash; - bytes32 expectedCouncilBudgetHash; + uint256 expectedGovernanceFundId; + uint256 expectedCouncilBudgetId; bytes governanceFundVotingModuleData; bytes councilBudgetVotingModuleData; @@ -2442,7 +2423,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I optionsAmounts[1] = 200 ether; // Create one proposal for each type - (expectedGovernanceFundHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + (expectedGovernanceFundId, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2451,7 +2432,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I governanceFundProposalDescription, governanceFundProposalType ); - (expectedCouncilBudgetHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + (expectedCouncilBudgetId, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2478,12 +2459,12 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I APPROVAL_VOTING_MODULE_ID ) ), - abi.encode(uint256(expectedGovernanceFundHash)) + abi.encode(uint256(expectedGovernanceFundId)) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedGovernanceFundHash, approvedProposer); + emit ProposalMovedToVote(expectedGovernanceFundId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2498,7 +2479,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I ); // Check that the proposal is in voting - (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundHash); + (,, bool movedToVote,,) = validator.getProposalData(expectedGovernanceFundId); assertTrue(movedToVote, "Proposal should be in voting"); } @@ -2518,12 +2499,12 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I APPROVAL_VOTING_MODULE_ID ) ), - abi.encode(uint256(expectedCouncilBudgetHash)) + abi.encode(uint256(expectedCouncilBudgetId)) ); // Expect the ProposalMovedToVote event to be emitted vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedCouncilBudgetHash, approvedProposer); + emit ProposalMovedToVote(expectedCouncilBudgetId, approvedProposer); // Move to vote vm.warp(START_TIMESTAMP + 1); @@ -2548,8 +2529,8 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string[] optionsDescriptions; address[] optionsRecipients; uint256[] optionsAmounts; - bytes32 governanceFundExpectedHash; - bytes32 councilBudgetExpectedHash; + uint256 governanceFundExpectedId; + uint256 councilBudgetExpectedId; bytes governanceFundVotingModuleData; bytes councilBudgetVotingModuleData; @@ -2557,7 +2538,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat super.setUp(); (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(1); - (governanceFundExpectedHash, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( + (governanceFundExpectedId, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2566,7 +2547,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat governanceFundProposalDescription, governanceFundProposalType ); - (councilBudgetExpectedHash, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( + (councilBudgetExpectedId, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( approvedProposer, criteriaValue, optionsDescriptions, @@ -2600,7 +2581,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ) public { - // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal hash it will + // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal ID it will // not find the proposal vm.assume(_criteriaValue != criteriaValue); @@ -2642,13 +2623,13 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat if (validProposalType == governanceFundProposalType) { // Set proposal data proposal type to a different value validator.setProposalData( - governanceFundExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + governanceFundExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data proposal type to a different value validator.setProposalData( - councilBudgetExpectedHash, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + councilBudgetExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); proposalDescription = councilBudgetProposalDescription; } @@ -2676,13 +2657,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string memory proposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData( - governanceFundExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER - ); + validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); proposalDescription = councilBudgetProposalDescription; } @@ -2704,11 +2683,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat string memory proposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data movedToVote to true - validator.setProposalData(governanceFundExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); proposalDescription = governanceFundProposalDescription; } else { // Set proposal data movedToVote to true - validator.setProposalData(councilBudgetExpectedHash, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); proposalDescription = councilBudgetProposalDescription; } @@ -2822,11 +2801,11 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( uint8 _proposalTypeValue, - bytes32 _randomHash + uint256 _randomId ) public { - vm.assume(_randomHash != governanceFundExpectedHash && _randomHash != councilBudgetExpectedHash); + vm.assume(_randomId != governanceFundExpectedId && _randomId != councilBudgetExpectedId); // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); @@ -2852,7 +2831,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat IOptimismGovernor.proposeWithModule, (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomHash)) + abi.encode(uint256(_randomId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); @@ -2995,11 +2974,11 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init public view { - bytes32 hash = validator.hashProposalWithModule(module, proposalData, descriptionHash); - bytes32 expectedHash = - keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash)); + uint256 id = validator.hashProposalWithModule(module, proposalData, descriptionHash); + uint256 expectedId = + uint256(keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash))); - assertEq(hash, expectedHash); + assertEq(id, expectedId); } function test_hashProposalWithModule_differentInputs_succeeds() public { @@ -3008,9 +2987,9 @@ contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init bytes memory data = abi.encode("data"); bytes32 descHash = keccak256("desc"); - bytes32 hash1 = validator.hashProposalWithModule(module1, data, descHash); - bytes32 hash2 = validator.hashProposalWithModule(module2, data, descHash); + uint256 id1 = validator.hashProposalWithModule(module1, data, descHash); + uint256 id2 = validator.hashProposalWithModule(module2, data, descHash); - assertTrue(hash1 != hash2); + assertTrue(id1 != id2); } } From ed61d76b26e21fb2295d6dbe01e09e518e8cc65f Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:20:27 +0300 Subject: [PATCH 67/73] fix: descrepancies (#464) * fix: improve comment * fix: make schemas immutable again * fix: improve code * fix: validation check order * fix: add move to vote safety check * fix: pre-pr --- .../governance/IProposalValidator.sol | 10 +- .../snapshots/abi/ProposalValidator.json | 72 +++++++------- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 22 +---- .../src/governance/ProposalValidator.sol | 99 ++++++++++++------- .../test/governance/ProposalValidator.t.sol | 57 +++++++++-- 6 files changed, 160 insertions(+), 104 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 291ba4833b0..84518b450dc 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -172,8 +172,6 @@ interface IProposalValidator is ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) external; @@ -190,9 +188,9 @@ interface IProposalValidator is ISemver { function initVersion() external view returns (uint8); - function approvedProposerAttestationSchemaUid() external view returns (bytes32); + function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); - function topDelegatesAttestationSchemaUid() external view returns (bytes32); + function TOP_DELEGATES_ATTESTATION_SCHEMA_UID() external view returns (bytes32); function OPTIMISTIC_MODULE_PERCENT_DIVISOR() external view returns (uint256); @@ -206,6 +204,8 @@ interface IProposalValidator is ISemver { ); function __constructor__( - IOptimismGovernor _governor + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid ) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index b26b2647557..f7c9631976a 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -5,11 +5,34 @@ "internalType": "contract IOptimismGovernor", "name": "_governor", "type": "address" + }, + { + "internalType": "bytes32", + "name": "_approvedProposerAttestationSchemaUid", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_topDelegatesAttestationSchemaUid", + "type": "bytes32" } ], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "GOVERNOR", @@ -36,6 +59,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TOP_DELEGATES_ATTESTATION_SCHEMA_UID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -54,19 +90,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "approvedProposerAttestationSchemaUid", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "initVersion", @@ -112,16 +135,6 @@ "name": "_proposalDistributionThreshold", "type": "uint256" }, - { - "internalType": "bytes32", - "name": "_approvedProposerAttestationSchemaUid", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "_topDelegatesAttestationSchemaUid", - "type": "bytes32" - }, { "internalType": "enum ProposalValidator.ProposalType[]", "name": "_proposalTypes", @@ -502,19 +515,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "topDelegatesAttestationSchemaUid", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 9f4dc118d5c..fe56ec278de 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x4171ca5a66e60a2a3715dc3108d76f985eb16937d71197c74ee0465461e8f9fe", - "sourceCodeHash": "0xa95d144dad3bb25ef0ac0e4ae9039b1ee5304e86e9ef7c1726d263f63598f864" + "initCodeHash": "0xd96122c73104bff67f8493e0e0d9d7aeeb6a631ecd52d5572a8a21b724591ad9", + "sourceCodeHash": "0x857b3e452c59b2263d812868916e20aafb81380f1438616d913141ae45a6b806" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 2b847ae3993..167b206ea9c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -34,46 +34,32 @@ "slot": "52", "type": "uint256[49]" }, - { - "bytes": "32", - "label": "approvedProposerAttestationSchemaUid", - "offset": 0, - "slot": "101", - "type": "bytes32" - }, - { - "bytes": "32", - "label": "topDelegatesAttestationSchemaUid", - "offset": 0, - "slot": "102", - "type": "bytes32" - }, { "bytes": "32", "label": "proposalDistributionThreshold", "offset": 0, - "slot": "103", + "slot": "101", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "104", + "slot": "102", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "105", + "slot": "103", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "106", + "slot": "104", "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 61db34720bb..01c6e0d28c3 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -134,7 +134,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Emitted when the proposal type data is set. /// @param proposalType The type of proposal. /// @param requiredApprovals The required number of approvals. - /// @param idInConfigurator The proposal type ID. + /// @param idInConfigurator The proposal type ID in the ProposalTypesConfigurator contract. event ProposalTypeDataSet(ProposalType proposalType, uint256 requiredApprovals, uint8 idInConfigurator); /// @notice Emitted with ProposalSubmitted event. @@ -228,11 +228,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is an approved proposer. /// @dev Schema format: { proposalType: uint8, date: string } - bytes32 public approvedProposerAttestationSchemaUid; + bytes32 public immutable APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; /// @notice The schema UID for attestations in the Ethereum Attestation Service for checking if the caller /// is part of the top100 delegates. - bytes32 public topDelegatesAttestationSchemaUid; + bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; /// @notice The max amount of tokens that can be distributed in a proposal. uint256 public proposalDistributionThreshold; @@ -248,8 +248,22 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Constructs the ProposalValidator contract. /// @param _governor The Optimism Governor contract address. - constructor(IOptimismGovernor _governor) ReinitializableBase(1) { + /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in the Ethereum Attestation Service + /// for checking if the caller + /// is an approved proposer. + /// @param _topDelegatesAttestationSchemaUid The schema UID for attestations in the Ethereum Attestation Service for + /// checking if the caller + /// is part of the top100 delegates. + constructor( + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid + ) + ReinitializableBase(1) + { GOVERNOR = _governor; + APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; + TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; _disableInitializers(); } @@ -269,8 +283,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { uint256 _duration, uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData ) @@ -281,9 +293,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - approvedProposerAttestationSchemaUid = _approvedProposerAttestationSchemaUid; - topDelegatesAttestationSchemaUid = _topDelegatesAttestationSchemaUid; - _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); @@ -452,17 +461,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator; + // Get the module address from the configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[ProposalType.CouncilMemberElections].idInConfigurator); - address votingModule = proposalTypeConfig.module; + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); // Validate voting module exists if (bytes(proposalTypeConfig.name).length == 0) { revert ProposalValidator_InvalidVotingModule(); } + address votingModule = proposalTypeConfig.module; + // Generate unique proposal ID proposalId_ = _hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription))); @@ -551,15 +563,21 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { bytes memory proposalVotingModuleData = abi.encode(options, approvalSettings); + // Retrieve the ID to use in the proposal type configurator + uint8 idInConfigurator = proposalTypesData[_proposalType].idInConfigurator; + // Get the module address from the configurator - IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = IProposalTypesConfigurator( - GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR() - ).proposalTypes(proposalTypesData[_proposalType].idInConfigurator); - address votingModule = proposalTypeConfig.module; + address votingModule; + { + IProposalTypesConfigurator.ProposalType memory proposalTypeConfig = + IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator); - // Validate voting module exists - if (bytes(proposalTypeConfig.name).length == 0) { - revert ProposalValidator_InvalidVotingModule(); + // Validate voting module exists + if (bytes(proposalTypeConfig.name).length == 0) { + revert ProposalValidator_InvalidVotingModule(); + } + + votingModule = proposalTypeConfig.module; } // Generate unique proposal ID @@ -707,13 +725,18 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (uint256 proposalId_) { + uint256 optionsLength = _optionsDescriptions.length; + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(_optionsDescriptions.length), + maxApprovals: uint8(optionsLength), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: _criteriaValue, @@ -808,6 +831,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidFundingProposalType(); } + uint256 optionsLength = _optionsDescriptions.length; + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } + // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); @@ -850,18 +879,20 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalAlreadyMovedToVote(); } - // Check if proposal can be moved to vote - VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; - if ( - votingCycleData.startingTimestamp > block.timestamp - || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp - ) { - revert ProposalValidator_InvalidVotingCycle(); - } + { + // Check if proposal can be moved to vote + VotingCycleData memory votingCycleData = votingCycles[proposal.votingCycle]; + if ( + votingCycleData.startingTimestamp > block.timestamp + || votingCycleData.startingTimestamp + votingCycleData.duration < block.timestamp + ) { + revert ProposalValidator_InvalidVotingCycle(); + } - // Check if total budget is within the voting cycle distribution limit - if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { - revert ProposalValidator_ExceedsDistributionThreshold(); + // Check if total budget is within the voting cycle distribution limit + if (votingCycleData.movedToVoteTokenCount + totalBudget > votingCycleData.votingCycleDistributionLimit) { + revert ProposalValidator_ExceedsDistributionThreshold(); + } } // Move proposal to vote @@ -951,7 +982,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { (uint8 proposalType,) = abi.decode(attestation.data, (uint8, string)); if ( - attestation.attester != owner() || attestation.schema != approvedProposerAttestationSchemaUid + attestation.attester != owner() || attestation.schema != APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID || attestation.recipient != _msgSender() || proposalType != uint8(_expectedProposalType) ) { revert ProposalValidator_InvalidAttestation(); @@ -975,7 +1006,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { } // check if the schema is correct - if (attestation.schema != topDelegatesAttestationSchemaUid) { + if (attestation.schema != TOP_DELEGATES_ATTESTATION_SCHEMA_UID) { revert ProposalValidator_InvalidAttestationSchema(); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index a307afbb918..81d24cb0c27 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -35,7 +35,13 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @title ProposalValidatorForTest /// @notice A test contract that exposes the private _hashProposalWithModule function contract ProposalValidatorForTest is ProposalValidator { - constructor(IOptimismGovernor _governor) ProposalValidator(_governor) { } + constructor( + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid + ) + ProposalValidator(_governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) + { } function hashProposalWithModule( address _module, @@ -516,7 +522,9 @@ contract ProposalValidator_Init is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest(governor); + impl = new ProposalValidatorForTest( + governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.prank(owner); @@ -531,8 +539,6 @@ contract ProposalValidator_Init is CommonTest { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -626,7 +632,9 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest(governor); + impl = new ProposalValidatorForTest( + governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID + ); validator = ProposalValidatorForTest(address(new Proxy(owner))); } @@ -648,8 +656,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -717,8 +723,6 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { DURATION, DISTRIBUTION_LIMIT, DISTRIBUTION_THRESHOLD, - APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, - TOP_DELEGATES_ATTESTATION_SCHEMA_UID, proposalTypes, proposalTypesData ) @@ -2336,6 +2340,16 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); } + function test_moveToVoteCouncilMemberElectionsProposal_invalidOptionsLength_reverts() public { + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](0), proposalDescription); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](256), proposalDescription); + } + function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp validator.setProposalData(expectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); @@ -2649,6 +2663,31 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } + function test_moveToVoteFundingProposal_invalidOptionsLength_reverts(uint8 _proposalTypeValue) public { + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, new string[](0), optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, new string[](256), optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); From 4e212e5e397706309b4d1f83d3b3fdfaf7f477e3 Mon Sep 17 00:00:00 2001 From: OneTony Date: Mon, 28 Jul 2025 22:03:34 +0300 Subject: [PATCH 68/73] fix: add total budget overflow check --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 ++ .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 9 +++- .../test/governance/ProposalValidator.t.sol | 49 +++++++++++++++++++ 5 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 84518b450dc..5208757dfc4 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -31,6 +31,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidProposer(); error ProposalValidator_InvalidProposal(); error ProposalValidator_InvalidVotingModule(); + error ProposalValidator_InvalidTotalBudget(); error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); event ProposalSubmitted( diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index f7c9631976a..f2305002325 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -829,6 +829,11 @@ "name": "ProposalValidator_InvalidProposer", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_InvalidTotalBudget", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_InvalidUpgradeProposalType", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index fe56ec278de..59c56aa2082 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd96122c73104bff67f8493e0e0d9d7aeeb6a631ecd52d5572a8a21b724591ad9", - "sourceCodeHash": "0x857b3e452c59b2263d812868916e20aafb81380f1438616d913141ae45a6b806" + "initCodeHash": "0xfd63a8de6795e33cba8b47ce2cab24f09a003065292d74bdbe236161335b02ba", + "sourceCodeHash": "0xa774418504002f9f2aaff0333a534c65a46e8bb9d9d1e9408fbe1b3204b10462" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 01c6e0d28c3..a1d9a76d09c 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -89,9 +89,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the proposal is invalid trying to move to vote. error ProposalValidator_InvalidProposal(); - /// @notice Thrown when the voting module address is invalid (zero address). + /// @notice Thrown when the voting module address is invalid. error ProposalValidator_InvalidVotingModule(); + /// @notice Thrown when the total budget is invalid (must be > 0 and <= uint128 max). + error ProposalValidator_InvalidTotalBudget(); + /// @notice Thrown when the attestation was created after the last voting cycle. error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); @@ -1084,6 +1087,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { description: _optionDescriptions[i] }); } + + if (totalBudget_ > type(uint128).max) { + revert ProposalValidator_InvalidTotalBudget(); + } } /// @notice Calculate `proposalId` based on `module`, `proposalData` and `descriptionHash`. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 81d24cb0c27..be1305762c1 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1855,6 +1855,26 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER ); } + + function test_submitFundingProposal_invalidTotalBudget_reverts(uint8 proposalTypeValue, uint256 _amount) public { + _amount = bound(_amount, type(uint136).max, type(uint192).max); + // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) + proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = + _createMinimalFundingArrays(1); + + vm.prank(owner); + validator.setProposalDistributionThreshold(type(uint256).max); + + amounts[0] = _amount; + vm.expectRevert(ProposalValidator.ProposalValidator_InvalidTotalBudget.selector); + vm.prank(user); + validator.submitFundingProposal( + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + ); + } } /// @title ProposalValidator_ApproveProposal_Test @@ -2688,6 +2708,35 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat ); } + function test_moveToVoteFundingProposal_invalidTotalBudget_reverts( + uint8 _proposalTypeValue, + uint256 _amount + ) + public + { + _amount = bound(_amount, type(uint136).max, type(uint192).max); + // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) + _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + + string memory proposalDescription; + if (proposalType == governanceFundProposalType) { + proposalDescription = governanceFundProposalDescription; + } else { + proposalDescription = councilBudgetProposalDescription; + } + + vm.prank(owner); + validator.setProposalDistributionThreshold(type(uint256).max); + + optionsAmounts[0] = _amount; + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidTotalBudget.selector); + vm.prank(approvedProposer); + validator.moveToVoteFundingProposal( + criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + ); + } + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); From ca8fb7d8b56a95d9114faf1bfc96cbc2c569fea9 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:26:54 +0300 Subject: [PATCH 69/73] fix: add approval timing check (#472) * fix: add approval timing check * fix: add missing test --- .../governance/IProposalValidator.sol | 1 + .../snapshots/abi/ProposalValidator.json | 5 + .../snapshots/semver-lock.json | 8 +- .../src/governance/ProposalValidator.sol | 12 +- .../test/governance/ProposalValidator.t.sol | 112 +++++++++++++++--- 5 files changed, 114 insertions(+), 24 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 5208757dfc4..42572a27be2 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -33,6 +33,7 @@ interface IProposalValidator is ISemver { error ProposalValidator_InvalidVotingModule(); error ProposalValidator_InvalidTotalBudget(); error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + error ProposalValidator_PreviousVotingCycleNotStarted(); event ProposalSubmitted( uint256 indexed proposalId, diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index f2305002325..1ae822e5390 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -849,6 +849,11 @@ "name": "ProposalValidator_InvalidVotingModule", "type": "error" }, + { + "inputs": [], + "name": "ProposalValidator_PreviousVotingCycleNotStarted", + "type": "error" + }, { "inputs": [], "name": "ProposalValidator_ProposalAlreadyApproved", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 17d6828ea77..d4e56adb4f1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,12 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xfd63a8de6795e33cba8b47ce2cab24f09a003065292d74bdbe236161335b02ba", - "sourceCodeHash": "0xa774418504002f9f2aaff0333a534c65a46e8bb9d9d1e9408fbe1b3204b10462" - }, - "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xfd63a8de6795e33cba8b47ce2cab24f09a003065292d74bdbe236161335b02ba", - "sourceCodeHash": "0xa774418504002f9f2aaff0333a534c65a46e8bb9d9d1e9408fbe1b3204b10462" + "initCodeHash": "0x07538dde1aeaef48d7a8dbe5f5abdc34d44167f3cfb62340f42f30aa1a955322", + "sourceCodeHash": "0x874017b054fa1593ddb9136fc92b1f827c4201fda11af930ee409e2c513028d0" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index a1d9a76d09c..5ca7d1673ea 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -98,6 +98,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Thrown when the attestation was created after the last voting cycle. error ProposalValidator_AttestationCreatedAfterLastVotingCycle(); + /// @notice Thrown when trying to approve and the previous voting cycle has not started. + error ProposalValidator_PreviousVotingCycleNotStarted(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -640,6 +643,12 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { previousVotingCycle = proposal.votingCycle; } + // revert if the previous voting cycle has not started, we should only allow delegates + // to approve relative close to the proposals voting cycle + if (votingCycles[previousVotingCycle].startingTimestamp > block.timestamp) { + revert ProposalValidator_PreviousVotingCycleNotStarted(); + } + // validate the attestation _validateTopDelegateAttestation(_attestationUid, previousVotingCycle); @@ -1020,8 +1029,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // since the attestations are updated daily we should only allow attestations // created before the last voting cycle of the proposal - // check if attestation was created after the previous voting cycle - if (attestation.time > previousVotingCycleData.startingTimestamp + previousVotingCycleData.duration) { + if (attestation.time > previousVotingCycleData.startingTimestamp) { revert ProposalValidator_AttestationCreatedAfterLastVotingCycle(); } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index be1305762c1..280d1fd2a42 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -1883,10 +1883,12 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { function setUp() public override { super.setUp(); - // create a new voting cycle + // create the previous voting cycle // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle vm.prank(owner); validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to the start of the previous cycle + vm.warp(START_TIMESTAMP - DURATION); } function test_approveProposal_succeeds(uint256 _proposalId, uint8 proposalTypeValue) public { @@ -1904,6 +1906,11 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { vm.expectEmit(address(validator)); emit ProposalApproved(_proposalId, topDelegate_A); + // warp to the start of current cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } + // Approve the proposal, use the attestation of the top delegate that was created in setUp vm.prank(topDelegate_A); validator.approveProposal(_proposalId, topDelegateAttestation_A); @@ -1921,6 +1928,13 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { function setUp() public override { super.setUp(); + + // create the previous voting cycle + // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle + vm.prank(owner); + validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to the start of the previous cycle + vm.warp(START_TIMESTAMP - DURATION); } function test_approveProposal_proposalDoesNotExist_reverts(uint256 _proposalId) public { @@ -1966,6 +1980,26 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalId, topDelegateAttestation_A); } + function test_approveProposal_previousVotingCycleNotStarted_reverts( + uint256 _proposalId, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + // set proposal data so that the proposal exists + validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + + // warp before the start of the previous cycle so that it reverts + vm.warp(START_TIMESTAMP - DURATION - 1); + + vm.expectRevert(IProposalValidator.ProposalValidator_PreviousVotingCycleNotStarted.selector); + vm.prank(topDelegate_A); + validator.approveProposal(_proposalId, topDelegateAttestation_A); + } + function test_approveProposal_invalidVotingCycle_reverts( uint256 _proposalId, uint8 proposalTypeValue, @@ -1983,6 +2017,11 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // set proposal data so that the proposal exists validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, votingCycle); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } + vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); validator.approveProposal(_proposalId, topDelegateAttestation_A); @@ -2017,9 +2056,10 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // set proposal data so that the proposal exists validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // set the voting cycle data of the previous cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); @@ -2032,9 +2072,10 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); // set proposal data so that the proposal exists validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // set the voting cycle data of the previous cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } // revoke the attestation vm.prank(owner); @@ -2050,6 +2091,42 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { validator.approveProposal(_proposalId, topDelegateAttestation_A); } + function test_approveProposal_attestationCreatedAfterPreviousVotingCycle_reverts( + uint256 _proposalId, + uint8 proposalTypeValue + ) + public + { + // Bound the proposal type to valid enum values (0-4) + proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + + // create a new delegate and attestation + address _delegate = makeAddr("delegate"); + bytes32 _attestationUid; + + // create the attestation based on the proposal type + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + // warp to after the start of the current voting cycle if the proposal is ProtocolOrGovernorUpgrade + // because this proposal can be submitted and approved outside of a voting cycle + vm.warp(START_TIMESTAMP + 1); + _attestationUid = _createTopDelegateAttestation(_delegate); + } else { + // warp to after the start of the previous cycle for an other proposal type + vm.warp(START_TIMESTAMP - DURATION + 1); + _attestationUid = _createTopDelegateAttestation(_delegate); + } + + // set proposal data so that the proposal exists + validator.setProposalData(_proposalId, _delegate, proposalType, false, 0, CYCLE_NUMBER); + + // warp to after the start of the current voting cycle + vm.warp(START_TIMESTAMP + 2); + vm.expectRevert(IProposalValidator.ProposalValidator_AttestationCreatedAfterLastVotingCycle.selector); + vm.prank(_delegate); + validator.approveProposal(_proposalId, _attestationUid); + } + function test_approveProposal_invalidAttestationCaller_reverts( uint256 _proposalId, uint8 proposalTypeValue, @@ -2066,9 +2143,10 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // set the voting cycle data of the previous cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); @@ -2088,9 +2166,10 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // set the voting cycle data of the previous cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } // create an attestation with partial delegation vm.prank(owner); @@ -2130,9 +2209,10 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Set mock proposal data of a random proposal in the validator contract validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); - // set the voting cycle data of the previous cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade + if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { + vm.warp(START_TIMESTAMP); + } // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); From f154e79b8ce7924a45b2b0824fffd44fcea2149d Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:04:27 +0300 Subject: [PATCH 70/73] fix: misc findings (#479) --- .../governance/IProposalValidator.sol | 4 - .../snapshots/abi/ProposalValidator.json | 20 ---- .../snapshots/semver-lock.json | 4 +- .../src/governance/ProposalValidator.sol | 110 ++++++------------ .../test/governance/ProposalValidator.t.sol | 58 ++------- 5 files changed, 49 insertions(+), 147 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index 42572a27be2..d09180e1c98 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -169,10 +169,6 @@ interface IProposalValidator is ISemver { function initialize( address _owner, - uint256 _cycleNumber, - uint256 _startingTimestamp, - uint256 _duration, - uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 1ae822e5390..68099896460 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -110,26 +110,6 @@ "name": "_owner", "type": "address" }, - { - "internalType": "uint256", - "name": "_cycleNumber", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_startingTimestamp", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_duration", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_votingCycleDistributionLimit", - "type": "uint256" - }, { "internalType": "uint256", "name": "_proposalDistributionThreshold", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index d4e56adb4f1..37f0b1238a1 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0x07538dde1aeaef48d7a8dbe5f5abdc34d44167f3cfb62340f42f30aa1a955322", - "sourceCodeHash": "0x874017b054fa1593ddb9136fc92b1f827c4201fda11af930ee409e2c513028d0" + "initCodeHash": "0xd97f520aa5e4b9b7536bc8fc6d655168d015c3b07161179ef399e7ca358a8e4f", + "sourceCodeHash": "0x78501a104a776d6b2b28e676c7907c0a7169962530df4d6915c9e618e63c5881" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 5ca7d1673ea..289dec0e0b3 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -240,7 +240,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// is part of the top100 delegates. bytes32 public immutable TOP_DELEGATES_ATTESTATION_SCHEMA_UID; - /// @notice The max amount of tokens that can be distributed in a proposal. + /// @notice The max amount of tokens that can be distributed in a single proposal. uint256 public proposalDistributionThreshold; /// @notice Mapping of voting cycle numbers to their corresponding data. @@ -275,19 +275,11 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Initializes the ProposalValidator contract. /// @param _owner The address that will own the contract. - /// @param _cycleNumber The number of the current voting cycle. - /// @param _startingTimestamp The starting timestamp of the voting cycle. - /// @param _duration The duration of the voting cycle. - /// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle. /// @param _proposalDistributionThreshold The max amount of tokens that can be distributed in a proposal. /// @param _proposalTypes Array of proposal types to set data for. /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. function initialize( address _owner, - uint256 _cycleNumber, - uint256 _startingTimestamp, - uint256 _duration, - uint256 _votingCycleDistributionLimit, uint256 _proposalDistributionThreshold, ProposalType[] memory _proposalTypes, ProposalTypeData[] memory _proposalTypesData @@ -299,7 +291,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - _setVotingCycleData(_cycleNumber, _startingTimestamp, _duration, _votingCycleDistributionLimit); _setProposalDistributionThreshold(_proposalDistributionThreshold); for (uint256 i = 0; i < _proposalTypes.length; i++) { @@ -400,17 +391,9 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // MaintenanceUpgrade proposals move directly to voting (atomic operation) if (_proposalType == ProposalType.MaintenanceUpgrade) { proposal.movedToVote = true; - - uint256 proposalId = GOVERNOR.proposeWithModule( - votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator + _proposeToGovernor( + votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator, proposalId_ ); - - // Make sure the proposalId matches - if (proposalId != proposalId_) { - revert ProposalValidator_ProposalIdMismatch(); - } - - emit ProposalMovedToVote(proposalId_, _msgSender()); } } @@ -441,14 +424,8 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Validate EAS attestation - must be called by owner-approved address _validateApprovedProposerAttestation(_attestationUid, ProposalType.CouncilMemberElections); - // Validate options length bounds - uint256 optionsLength = _optionDescriptions.length; - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert ProposalValidator_InvalidOptionsLength(); - } - // Validate criteria value doesn't exceed options length for TopChoices - if (_criteriaValue > optionsLength) { + if (_criteriaValue > _optionDescriptions.length) { revert ProposalValidator_InvalidCriteriaValue(); } @@ -458,7 +435,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { // Configure approval voting settings with TopChoices criteria IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(optionsLength), + maxApprovals: uint8(_optionDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), // No budget token for elections criteriaValue: _criteriaValue, @@ -549,11 +526,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_ProposalTypesDataLengthMismatch(); } - // Validate options length bounds - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert ProposalValidator_InvalidOptionsLength(); - } - // Build proposal options with funding execution data (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); @@ -713,15 +685,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = - GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - - // Make sure the proposalId matches - if (proposalId != proposalId_) { - revert ProposalValidator_ProposalIdMismatch(); - } - - emit ProposalMovedToVote(proposalId_, _msgSender()); + _proposeToGovernor(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator, proposalId_); } /// @notice Moves a council member elections proposal to vote by proposing it on the Governor. @@ -737,18 +701,13 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { external returns (uint256 proposalId_) { - uint256 optionsLength = _optionsDescriptions.length; - // Validate options length bounds - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert ProposalValidator_InvalidOptionsLength(); - } // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options,) = _buildApprovalModuleOptions(_optionsDescriptions, new address[](0), new uint256[](0)); // Configure approval module settings IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(optionsLength), + maxApprovals: uint8(_optionsDescriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), criteriaValue: _criteriaValue, @@ -804,15 +763,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposal.movedToVote = true; // Propose with module on the Governor - uint256 proposalId = - GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator); - - // Make sure the proposalId matches - if (proposalId != proposalId_) { - revert ProposalValidator_ProposalIdMismatch(); - } - - emit ProposalMovedToVote(proposalId_, _msgSender()); + _proposeToGovernor(votingModule, proposalVotingModuleData, _proposalDescription, idInConfigurator, proposalId_); } /// @notice Moves a funding proposal to vote by proposing it on the Governor. @@ -843,12 +794,6 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { revert ProposalValidator_InvalidFundingProposalType(); } - uint256 optionsLength = _optionsDescriptions.length; - // Validate options length bounds - if (optionsLength == 0 || optionsLength > type(uint8).max) { - revert ProposalValidator_InvalidOptionsLength(); - } - // Configure approval module options (IApprovalVotingModule.ProposalOption[] memory options, uint256 totalBudget) = _buildApprovalModuleOptions(_optionsDescriptions, _optionsRecipients, _optionsAmounts); @@ -912,15 +857,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { votingCycles[proposal.votingCycle].movedToVoteTokenCount += totalBudget; // Propose with module on the Governor - uint256 proposalId = - GOVERNOR.proposeWithModule(votingModule, proposalVotingModuleData, _description, idInConfigurator); - - // Make sure the proposalId matches - if (proposalId != proposalId_) { - revert ProposalValidator_ProposalIdMismatch(); - } - - emit ProposalMovedToVote(proposalId_, _msgSender()); + _proposeToGovernor(votingModule, proposalVotingModuleData, _description, idInConfigurator, proposalId_); } /// @notice Sets the data of a voting cycle. @@ -1057,6 +994,10 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { returns (IApprovalVotingModule.ProposalOption[] memory options_, uint256 totalBudget_) { uint256 optionsLength = _optionDescriptions.length; + // Validate options length bounds + if (optionsLength == 0 || optionsLength > type(uint8).max) { + revert ProposalValidator_InvalidOptionsLength(); + } options_ = new IApprovalVotingModule.ProposalOption[](optionsLength); for (uint256 i = 0; i < optionsLength; i++) { @@ -1160,4 +1101,29 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { proposalTypesData[_proposalType] = _proposalTypeData; emit ProposalTypeDataSet(_proposalType, _proposalTypeData.requiredApprovals, _proposalTypeData.idInConfigurator); } + + /// @notice Private function to propose to the governor when a proposal is ready to be moved to vote. + /// @param _votingModule The address of the voting module to use for this proposal. + /// @param _proposalData The proposal data to pass to the voting module. + /// @param _description The description of the proposal. + /// @param _idInConfigurator The ID of the proposal type in the proposal types configurator. + /// @param _expectedProposalId The proposalId should be the same as the one returned by the governor. + function _proposeToGovernor( + address _votingModule, + bytes memory _proposalData, + string memory _description, + uint8 _idInConfigurator, + uint256 _expectedProposalId + ) + private + { + uint256 proposalId = GOVERNOR.proposeWithModule(_votingModule, _proposalData, _description, _idInConfigurator); + + // Make sure the proposalId matches + if (proposalId != _expectedProposalId) { + revert ProposalValidator_ProposalIdMismatch(); + } + + emit ProposalMovedToVote(_expectedProposalId, _msgSender()); + } } diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 280d1fd2a42..a9c3c60150f 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -527,23 +527,14 @@ contract ProposalValidator_Init is CommonTest { ); validator = ProposalValidatorForTest(address(new Proxy(owner))); - vm.prank(owner); + vm.startPrank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - CYCLE_NUMBER, - START_TIMESTAMP, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) + abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); + // set the data for one voting cycle + validator.setVotingCycleData(CYCLE_NUMBER, START_TIMESTAMP, DURATION, DISTRIBUTION_THRESHOLD); + vm.stopPrank(); } /// @dev Sets up the test suite. @@ -647,33 +638,13 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { vm.prank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - CYCLE_NUMBER, - START_TIMESTAMP, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) + abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); // Verify initialization was successful assertEq(validator.proposalDistributionThreshold(), DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); - // Verify voting cycle data - (uint256 startingTimestamp, uint256 duration, uint256 distributionLimit, uint256 movedToVoteTokenCount) = - validator.votingCycles(CYCLE_NUMBER); - assertEq(startingTimestamp, START_TIMESTAMP); - assertEq(duration, DURATION); - assertEq(distributionLimit, DISTRIBUTION_LIMIT); - assertEq(movedToVoteTokenCount, 0); - // Verify proposal type data for (uint256 i = 0; i < proposalTypes.length; i++) { (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalTypes[i]); @@ -714,19 +685,7 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall( - impl.initialize, - ( - owner, - CYCLE_NUMBER, - START_TIMESTAMP, - DURATION, - DISTRIBUTION_LIMIT, - DISTRIBUTION_THRESHOLD, - proposalTypes, - proposalTypesData - ) - ) + abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); } } @@ -1319,11 +1278,12 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop function test_submitCouncilMemberElectionsProposal_zeroOptions_reverts() public { string[] memory emptyOptions = new string[](0); + uint128 zeroCriteriaValue = 0; // 0 so it doesnt exceed the options length vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER + zeroCriteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER ); } From 2e40422d87b40db62cdaf2816b4ed366e7f7dbee Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:37:58 +0300 Subject: [PATCH 71/73] fix: improve test vars names (#480) * fix: improve test vars names * fix: test helpers input names --- .../test/governance/ProposalValidator.t.sol | 1333 ++++++++--------- 1 file changed, 655 insertions(+), 678 deletions(-) diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index a9c3c60150f..83e0da1a2a6 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -100,22 +100,28 @@ contract ProposalValidatorForTest is ProposalValidator { } } -/// @title ProposalValidator_Init +/// @title ProposalValidator_TestInit /// @notice Setup contract for ProposalValidator tests -contract ProposalValidator_Init is CommonTest { +contract ProposalValidator_TestInit is CommonTest { using stdStorage for StdStorage; + // voting cycle constants uint256 public constant CYCLE_NUMBER = 1; uint256 public constant START_TIMESTAMP = 1000000; uint256 public constant DURATION = 1 days; - uint256 public constant DISTRIBUTION_LIMIT = 20000 ether; - uint256 public constant DISTRIBUTION_THRESHOLD = 10000 ether; + uint256 public constant VOTING_CYCLE_DISTRIBUTION_LIMIT = 20000 ether; + + // proposal data constants + uint256 public constant PROPOSAL_DISTRIBUTION_THRESHOLD = 10000 ether; uint256 public constant PROPOSAL_REQUIRED_APPROVALS = 1; - uint256 public constant MINIMUM_VOTING_POWER = 10000 ether; uint256 public constant OPTIMISTIC_MODULE_PERCENT_DIVISOR = 10_000; uint8 public constant APPROVAL_VOTING_MODULE_ID = 1; uint8 public constant OPTIMISTIC_VOTING_MODULE_ID = 2; uint64 public constant ATT_EXPIRATION_TIME = 10 days; + uint248 public constant AGAINST_THRESHOLD = 5000; // 50% + string public constant PROPOSAL_DESCRIPTION = "Test proposal"; + + // attestation constants bytes32 public APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID; bytes32 public TOP_DELEGATES_ATTESTATION_SCHEMA_UID; @@ -284,10 +290,10 @@ contract ProposalValidator_Init is CommonTest { } function _constructFundingVotingModuleData( - string[] memory descriptions, - address[] memory recipients, - uint256[] memory amounts, - uint128 criteriaValue + string[] memory _descriptions, + address[] memory _recipients, + uint256[] memory _amounts, + uint128 _criteriaValue ) internal pure @@ -295,37 +301,37 @@ contract ProposalValidator_Init is CommonTest { { // Construct ProposalOption array IApprovalVotingModule.ProposalOption[] memory options = - new IApprovalVotingModule.ProposalOption[](descriptions.length); + new IApprovalVotingModule.ProposalOption[](_descriptions.length); - for (uint256 i = 0; i < descriptions.length; i++) { + for (uint256 i = 0; i < _descriptions.length; i++) { address[] memory targets = new address[](1); uint256[] memory values = new uint256[](1); bytes[] memory calldatas = new bytes[](1); targets[0] = Predeploys.GOVERNANCE_TOKEN; - calldatas[0] = abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i])); + calldatas[0] = abi.encodeCall(IERC20.transfer, (_recipients[i], _amounts[i])); options[i] = IApprovalVotingModule.ProposalOption({ - budgetTokensSpent: amounts[i], + budgetTokensSpent: _amounts[i], targets: targets, values: values, calldatas: calldatas, - description: descriptions[i] + description: _descriptions[i] }); } // Calculate total budget uint256 totalBudget = 0; - for (uint256 i = 0; i < amounts.length; i++) { - totalBudget += amounts[i]; + for (uint256 i = 0; i < _amounts.length; i++) { + totalBudget += _amounts[i]; } // Construct ProposalSettings IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(descriptions.length), + maxApprovals: uint8(_descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.Threshold), budgetToken: Predeploys.GOVERNANCE_TOKEN, - criteriaValue: criteriaValue, + criteriaValue: _criteriaValue, budgetAmount: uint128(totalBudget) }); @@ -334,8 +340,8 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper function to construct voting module data for council elections function _constructCouncilElectionVotingModuleData( - string[] memory descriptions, - uint128 criteriaValue + string[] memory _descriptions, + uint128 _criteriaValue ) internal pure @@ -343,9 +349,9 @@ contract ProposalValidator_Init is CommonTest { { // Construct ProposalOption array for elections (no execution calls) IApprovalVotingModule.ProposalOption[] memory options = - new IApprovalVotingModule.ProposalOption[](descriptions.length); + new IApprovalVotingModule.ProposalOption[](_descriptions.length); - for (uint256 i = 0; i < descriptions.length; i++) { + for (uint256 i = 0; i < _descriptions.length; i++) { address[] memory targets = new address[](0); uint256[] memory values = new uint256[](0); bytes[] memory calldatas = new bytes[](0); @@ -355,16 +361,16 @@ contract ProposalValidator_Init is CommonTest { targets: targets, values: values, calldatas: calldatas, - description: descriptions[i] + description: _descriptions[i] }); } // Construct ProposalSettings with TopChoices criteria IApprovalVotingModule.ProposalSettings memory approvalSettings = IApprovalVotingModule.ProposalSettings({ - maxApprovals: uint8(descriptions.length), + maxApprovals: uint8(_descriptions.length), criteria: uint8(IApprovalVotingModule.PassingCriteria.TopChoices), budgetToken: address(0), - criteriaValue: criteriaValue, + criteriaValue: _criteriaValue, budgetAmount: 0 }); @@ -372,32 +378,32 @@ contract ProposalValidator_Init is CommonTest { } /// @notice Helper function to construct voting module data for upgrade proposals - function _constructOptimisticVotingModuleData(uint248 againstThreshold) internal pure returns (bytes memory) { + function _constructOptimisticVotingModuleData(uint248 _againstThreshold) internal pure returns (bytes memory) { IOptimisticModule.ProposalSettings memory optimisticSettings = - IOptimisticModule.ProposalSettings({ againstThreshold: againstThreshold, isRelativeToVotableSupply: true }); + IOptimisticModule.ProposalSettings({ againstThreshold: _againstThreshold, isRelativeToVotableSupply: true }); return abi.encode(optimisticSettings); } /// @notice Helper function to create a proposal for move to vote function _createUpgradeProposalForMoveToVote( - address proposer, - uint248 againstThreshold, - string memory proposalDescription + address _proposer, + uint248 _againstThreshold, + string memory _proposalDescription ) internal returns (uint256 proposalId_, bytes memory votingModuleData_) { // Calculate expected proposal ID - votingModuleData_ = _constructOptimisticVotingModuleData(againstThreshold); + votingModuleData_ = _constructOptimisticVotingModuleData(_againstThreshold); proposalId_ = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData_, keccak256(bytes(_proposalDescription)) ); // 1 vote as default for being able to move to vote validator.setProposalData( proposalId_, - proposer, + _proposer, ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, false, PROPOSAL_REQUIRED_APPROVALS, @@ -407,22 +413,22 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper function to create a proposal for move to vote for council elections function _createCouncilElectionProposalForMoveToVote( - address proposer, - uint128 criteriaValue, - string[] memory optionsDescriptions, - string memory proposalDescription + address _proposer, + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + string memory _proposalDescription ) internal returns (uint256 proposalId_, bytes memory votingModuleData_) { - votingModuleData_ = _constructCouncilElectionVotingModuleData(optionsDescriptions, criteriaValue); + votingModuleData_ = _constructCouncilElectionVotingModuleData(_optionsDescriptions, _criteriaValue); proposalId_ = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + approvalVotingModule, votingModuleData_, keccak256(bytes(_proposalDescription)) ); validator.setProposalData( proposalId_, - proposer, + _proposer, ProposalValidator.ProposalType.CouncilMemberElections, false, PROPOSAL_REQUIRED_APPROVALS, @@ -432,24 +438,24 @@ contract ProposalValidator_Init is CommonTest { /// @notice Helper function to create a proposal for move to vote for a funding proposal type function _createFundingProposalForMoveToVote( - address proposer, - uint128 criteriaValue, - string[] memory optionsDescriptions, - address[] memory optionsRecipients, - uint256[] memory optionsAmounts, - string memory proposalDescription, - ProposalValidator.ProposalType proposalType + address _proposer, + uint128 _criteriaValue, + string[] memory _optionsDescriptions, + address[] memory _optionsRecipients, + uint256[] memory _optionsAmounts, + string memory _proposalDescription, + ProposalValidator.ProposalType _proposalType ) internal returns (uint256 proposalId_, bytes memory votingModuleData_) { votingModuleData_ = - _constructFundingVotingModuleData(optionsDescriptions, optionsRecipients, optionsAmounts, criteriaValue); + _constructFundingVotingModuleData(_optionsDescriptions, _optionsRecipients, _optionsAmounts, _criteriaValue); proposalId_ = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData_, keccak256(bytes(proposalDescription)) + approvalVotingModule, votingModuleData_, keccak256(bytes(_proposalDescription)) ); - validator.setProposalData(proposalId_, proposer, proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData(proposalId_, _proposer, _proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); } /// @notice Helper function to setup proposal types configurator mocks @@ -530,10 +536,10 @@ contract ProposalValidator_Init is CommonTest { vm.startPrank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) + abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); // set the data for one voting cycle - validator.setVotingCycleData(CYCLE_NUMBER, START_TIMESTAMP, DURATION, DISTRIBUTION_THRESHOLD); + validator.setVotingCycleData(CYCLE_NUMBER, START_TIMESTAMP, DURATION, VOTING_CYCLE_DISTRIBUTION_LIMIT); vm.stopPrank(); } @@ -608,7 +614,7 @@ contract ProposalValidator_Init is CommonTest { /// @title ProposalValidator_Version_Test /// @notice Tests for the version function -contract ProposalValidator_Version_Test is ProposalValidator_Init { +contract ProposalValidator_Version_Test is ProposalValidator_TestInit { function test_version_succeeds() public view { string memory versionString = validator.version(); assertEq(versionString, "1.0.0"); @@ -617,7 +623,7 @@ contract ProposalValidator_Version_Test is ProposalValidator_Init { /// @title ProposalValidator_Initialize_Test /// @notice Tests for the initialize function -contract ProposalValidator_Initialize_Test is ProposalValidator_Init { +contract ProposalValidator_Initialize_Test is ProposalValidator_TestInit { /// @dev Override to create validator proxy without initialization for testing function _initializeValidator() internal override { // Create mock addresses @@ -638,11 +644,11 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { vm.prank(owner); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) + abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); // Verify initialization was successful - assertEq(validator.proposalDistributionThreshold(), DISTRIBUTION_THRESHOLD); + assertEq(validator.proposalDistributionThreshold(), PROPOSAL_DISTRIBUTION_THRESHOLD); assertEq(validator.owner(), owner); // Verify proposal type data @@ -685,46 +691,42 @@ contract ProposalValidator_Initialize_Test is ProposalValidator_Init { vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); IProxy(payable(address(validator))).upgradeToAndCall( address(impl), - abi.encodeCall(impl.initialize, (owner, DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) + abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) ); } } /// @title ProposalValidator_SubmitUpgradeProposal_Test /// @notice Happy path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init { - string proposalDescription; - +contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_TestInit { function setUp() public override { super.setUp(); _setProtocolOrGovernorUpgradeProposalType(); _setMaintenanceUpgradeProposalType(); - - proposalDescription = "Protocol Upgrade Proposal"; } function testFuzz_submitUpgradeProposal_maintenanceUpgrade_succeeds( - uint248 againstThreshold, - address proposer + uint248 fuzzedAgainstThreshold, + address fuzzedProposer ) public { // Assume proposer is not zero address - vm.assume(proposer != address(0)); + vm.assume(fuzzedProposer != address(0)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Bound fuzzedAgainstThreshold to valid range (1 to 10000 basis points) + fuzzedAgainstThreshold = uint248(bound(fuzzedAgainstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + bytes32 fuzzedAttestationUid = _createApprovedProposerAttestation(fuzzedProposer, proposalType); // Calculate expected proposal ID - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes memory votingModuleData = _constructOptimisticVotingModuleData(fuzzedAgainstThreshold); uint256 expectedId = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) @@ -739,24 +741,24 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) + (optimisticVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(expectedId) ); // For MaintenanceUpgrade, events are: ProposalSubmitted, ProposalVotingModuleData, ProposalMovedToVote vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, fuzzedProposer, PROPOSAL_DESCRIPTION, proposalType); vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedId, votingModuleData); vm.expectEmit(address(validator)); - emit ProposalMovedToVote(expectedId, proposer); + emit ProposalMovedToVote(expectedId, fuzzedProposer); - vm.prank(proposer); + vm.prank(fuzzedProposer); uint256 proposalId = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + fuzzedAgainstThreshold, PROPOSAL_DESCRIPTION, fuzzedAttestationUid, proposalType, CYCLE_NUMBER ); assertEq(proposalId, expectedId); @@ -770,7 +772,7 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init uint256 votingCycle ) = validator.getProposalData(proposalId); - assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(storedProposer, fuzzedProposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); assertTrue(movedToVote, "MaintenanceUpgrade should be in voting immediately"); assertEq(approvalCount, 0, "Approval count should be 0"); @@ -778,26 +780,26 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init } function testFuzz_submitUpgradeProposal_protocolOrGovernorUpgrade_succeeds( - uint248 againstThreshold, - address proposer + uint248 fuzzedAgainstThreshold, + address fuzzedProposer ) public { // Assume proposer is not zero address - vm.assume(proposer != address(0)); + vm.assume(fuzzedProposer != address(0)); - // Bound againstThreshold to valid range (1 to 10000 basis points) - againstThreshold = uint248(bound(againstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); + // Bound fuzzedAgainstThreshold to valid range (1 to 10000 basis points) + fuzzedAgainstThreshold = uint248(bound(fuzzedAgainstThreshold, 1, OPTIMISTIC_MODULE_PERCENT_DIVISOR)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; // Create attestation for the proposal - bytes32 attestationUid = _createApprovedProposerAttestation(proposer, proposalType); + bytes32 attestationUid = _createApprovedProposerAttestation(fuzzedProposer, proposalType); // Calculate expected proposal ID - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes memory votingModuleData = _constructOptimisticVotingModuleData(fuzzedAgainstThreshold); uint256 expectedId = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) @@ -809,14 +811,14 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init // For ProtocolOrGovernorUpgrade, only ProposalSubmitted and ProposalVotingModuleData events vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedId, proposer, proposalDescription, proposalType); + emit ProposalSubmitted(expectedId, fuzzedProposer, PROPOSAL_DESCRIPTION, proposalType); vm.expectEmit(address(validator)); emit ProposalVotingModuleData(expectedId, votingModuleData); - vm.prank(proposer); + vm.prank(fuzzedProposer); uint256 proposalId = validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + fuzzedAgainstThreshold, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); assertEq(proposalId, expectedId); @@ -830,58 +832,42 @@ contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_Init uint256 votingCycle ) = validator.getProposalData(proposalId); - assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(storedProposer, fuzzedProposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); assertFalse(movedToVote, "ProtocolOrGovernorUpgrade should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } -} - -/// @title ProposalValidator_SubmitUpgradeProposal_TestFail -/// @notice Sad path tests for submitUpgradeProposal function -contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_Init { - string proposalDescription; - uint248 againstThreshold = 5000; // 50% - - function setUp() public override { - super.setUp(); - - _setProtocolOrGovernorUpgradeProposalType(); - _setMaintenanceUpgradeProposalType(); - - proposalDescription = "Test upgrade proposal"; - } - function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitUpgradeProposal_invalidProposalType_reverts(uint8 fuzzedProposalTypeValue) public { // Valid upgrade proposal types are ProtocolOrGovernorUpgrade (0) and MaintenanceUpgrade (1) - proposalTypeValue = uint8(bound(proposalTypeValue, 2, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 2, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidUpgradeProposalType.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } function testFuzz_submitUpgradeProposal_invalidVotingCycle_reverts( - uint8 proposalTypeValue, - uint256 votingCycle + uint8 fuzzedProposalTypeValue, + uint256 fuzzedVotingCycle ) public { - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - vm.assume(votingCycle != CYCLE_NUMBER); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 1)); + vm.assume(fuzzedVotingCycle != CYCLE_NUMBER); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, votingCycle + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, fuzzedVotingCycle ); } @@ -894,7 +880,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, fuzzedAttestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, fuzzedAttestationUid, proposalType, CYCLE_NUMBER ); } @@ -908,13 +894,13 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 proposalTypeValue) public { - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + function testFuzz_submitUpgradeProposal_attestationExpired_reverts(uint8 fuzzedProposalTypeValue) public { + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // warp the time to after the attestation expiration time @@ -922,7 +908,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } @@ -933,13 +919,15 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); - validator.submitUpgradeProposal(zeroThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER); + validator.submitUpgradeProposal(zeroThreshold, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER); } - function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 excessiveThreshold) public { + function testFuzz_submitUpgradeProposal_exceedsMaxAgainstThreshold_reverts(uint248 fuzzedExcessiveThreshold) + public + { // Bound excessive threshold to be greater than OPTIMISTIC_MODULE_PERCENT_DIVISOR - excessiveThreshold = - uint248(bound(excessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); + fuzzedExcessiveThreshold = + uint248(bound(fuzzedExcessiveThreshold, OPTIMISTIC_MODULE_PERCENT_DIVISOR + 1, type(uint248).max)); ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -947,7 +935,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAgainstThreshold.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - excessiveThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + fuzzedExcessiveThreshold, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } @@ -961,21 +949,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitUpgradeProposal_duplicateProposal_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal ID - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes memory votingModuleData = _constructOptimisticVotingModuleData(AGAINST_THRESHOLD); uint256 expectedId = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return 0 for first submission @@ -991,7 +979,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) + (optimisticVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(expectedId) ); @@ -1000,7 +988,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); // Attempt to submit identical proposal should revert @@ -1010,21 +998,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitUpgradeProposal_proposalExistsInGovernor_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal ID - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes memory votingModuleData = _constructOptimisticVotingModuleData(AGAINST_THRESHOLD); uint256 expectedId = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) @@ -1040,7 +1028,7 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } @@ -1068,14 +1056,14 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, invalidAttestation, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, invalidAttestation, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitUpgradeProposal_attestationRevoked_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only upgrade proposals (0 = ProtocolOrGovernorUpgrade, 1 = MaintenanceUpgrade) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 1)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 1)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create valid attestation first (make it revocable) bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); @@ -1092,21 +1080,21 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } - function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 proposalId) public { + function test_submitUpgradeProposal_proposalIdMismatch_reverts(uint256 fuzzedProposalId) public { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.MaintenanceUpgrade; bytes32 attestationUid = _createApprovedProposerAttestation(topDelegate_A, proposalType); // Calculate expected proposal ID - bytes memory votingModuleData = _constructOptimisticVotingModuleData(againstThreshold); + bytes memory votingModuleData = _constructOptimisticVotingModuleData(AGAINST_THRESHOLD); uint256 expectedId = validator.hashProposalWithModule( - optimisticVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + optimisticVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); - vm.assume(proposalId != expectedId); // Ensure proposalId is different from expectedId + vm.assume(fuzzedProposalId != expectedId); // Ensure proposalId is different from expectedId _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -1119,50 +1107,63 @@ contract ProposalValidator_SubmitUpgradeProposal_TestFail is ProposalValidator_I address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) + (optimisticVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(proposalId) + abi.encode(fuzzedProposalId) ); vm.expectRevert(ProposalValidator.ProposalValidator_ProposalIdMismatch.selector); vm.prank(topDelegate_A); validator.submitUpgradeProposal( - againstThreshold, proposalDescription, attestationUid, proposalType, CYCLE_NUMBER + AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION, attestationUid, proposalType, CYCLE_NUMBER ); } } /// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_Test /// @notice Happy path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_Init { - string proposalDescription; +contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is ProposalValidator_TestInit { + uint128 criteriaValue; + string[] optionDescriptions; + bytes32 approvedProposerAttestationUid; function setUp() public override { super.setUp(); _setCouncilMemberElectionsProposalType(); - proposalDescription = "Council Member Elections Q4 2024"; + criteriaValue = 2; + optionDescriptions = new string[](3); + optionDescriptions[0] = "Candidate A"; + optionDescriptions[1] = "Candidate B"; + optionDescriptions[2] = "Candidate C"; + + approvedProposerAttestationUid = + _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); } - function testFuzz_submitCouncilMemberElectionsProposal_succeeds(uint8 optionCount, uint128 criteriaValue) public { - optionCount = uint8(bound(optionCount, 2, 5)); // Minimum 2 options to have valid criteria < optionCount - criteriaValue = uint128(bound(criteriaValue, 1, optionCount - 1)); // Must be less than optionCount + function testFuzz_submitCouncilMemberElectionsProposal_succeeds( + uint8 fuzzedOptionCount, + uint128 fuzzedCriteriaValue + ) + public + { + fuzzedOptionCount = uint8(bound(fuzzedOptionCount, 2, 5)); // Minimum 2 options to have valid criteria < + // optionCount + fuzzedCriteriaValue = uint128(bound(fuzzedCriteriaValue, 1, fuzzedOptionCount - 1)); // Must be less than + // optionCount // Create dynamic array of option descriptions based on option count - string[] memory optionDescriptions = new string[](optionCount); - for (uint256 i = 0; i < optionCount; i++) { - optionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); + string[] memory fuzzedOptionDescriptions = new string[](fuzzedOptionCount); + for (uint256 i = 0; i < fuzzedOptionCount; i++) { + fuzzedOptionDescriptions[i] = string(abi.encodePacked("Candidate ", vm.toString(i))); } - // Create attestation for the proposal - bytes32 attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Calculate expected proposal ID - bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); + bytes memory votingModuleData = + _constructCouncilElectionVotingModuleData(fuzzedOptionDescriptions, fuzzedCriteriaValue); uint256 expectedId = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) @@ -1173,7 +1174,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal // Expect ProposalSubmitted event vm.expectEmit(address(validator)); emit ProposalSubmitted( - expectedId, topDelegate_A, proposalDescription, ProposalValidator.ProposalType.CouncilMemberElections + expectedId, topDelegate_A, PROPOSAL_DESCRIPTION, ProposalValidator.ProposalType.CouncilMemberElections ); // Expect ProposalVotingModuleData event @@ -1184,7 +1185,11 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal vm.prank(topDelegate_A); uint256 proposalId = validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + fuzzedCriteriaValue, + fuzzedOptionDescriptions, + PROPOSAL_DESCRIPTION, + approvedProposerAttestationUid, + CYCLE_NUMBER ); assertEq(proposalId, expectedId); @@ -1208,50 +1213,28 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_Test is Proposal assertEq(approvalCount, 0, "Approval count should be 0"); assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } -} - -/// @title ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail -/// @notice Sad path tests for submitCouncilMemberElectionsProposal function -contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - uint128 criteriaValue; - string[] optionDescriptions; - string proposalDescription; - bytes32 attestationUid; - - function setUp() public override { - super.setUp(); - - _setCouncilMemberElectionsProposalType(); - - criteriaValue = 2; - optionDescriptions = new string[](3); - optionDescriptions[0] = "Candidate A"; - optionDescriptions[1] = "Candidate B"; - optionDescriptions[2] = "Candidate C"; - - proposalDescription = "Test Council Elections"; - attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - } - function testFuzz_submitCouncilMemberElectionsProposal_invalidVotingCycle_reverts(uint256 votingCycle) public { - vm.assume(votingCycle != CYCLE_NUMBER); + function testFuzz_submitCouncilMemberElectionsProposal_invalidVotingCycle_reverts(uint256 fuzzedVotingCycle) + public + { + vm.assume(fuzzedVotingCycle != CYCLE_NUMBER); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, votingCycle + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, fuzzedVotingCycle ); } function testFuzz_submitCouncilMemberElectionsProposal_invalidAttestation_reverts(bytes32 fuzzedAttestationUid) public { - vm.assume(fuzzedAttestationUid != attestationUid); // Ensure it's different from valid attestation + vm.assume(fuzzedAttestationUid != approvedProposerAttestationUid); // Ensure it's different from valid + // attestation vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, fuzzedAttestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, fuzzedAttestationUid, CYCLE_NUMBER ); } @@ -1262,7 +1245,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(fuzzedProposer); // Different from attested topDelegate_A validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1272,7 +1255,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_AttestationExpired.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1283,7 +1266,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - zeroCriteriaValue, emptyOptions, proposalDescription, attestationUid, CYCLE_NUMBER + zeroCriteriaValue, emptyOptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1291,7 +1274,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); uint256 expectedId = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return 0 for first submission @@ -1304,7 +1287,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Submit first proposal vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); // Attempt to submit identical proposal should revert @@ -1314,7 +1297,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1322,7 +1305,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop // Calculate expected proposal ID bytes memory votingModuleData = _constructCouncilElectionVotingModuleData(optionDescriptions, criteriaValue); uint256 expectedId = validator.hashProposalWithModule( - approvalVotingModule, votingModuleData, keccak256(bytes(proposalDescription)) + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) @@ -1338,7 +1321,7 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1366,22 +1349,22 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, invalidAttestation, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, invalidAttestation, CYCLE_NUMBER ); } function testFuzz_submitCouncilMemberElectionsProposal_criteriaValueExceedsOptionsLength_reverts( - uint128 invalidCriteriaValue + uint128 fuzzedCriteriaValue ) public { - // Bound invalidCriteriaValue to be greater than options length - invalidCriteriaValue = uint128(bound(invalidCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); + // Bound fuzzedCriteriaValue to be greater than options length + fuzzedCriteriaValue = uint128(bound(fuzzedCriteriaValue, optionDescriptions.length + 1, type(uint128).max)); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidCriteriaValue.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - invalidCriteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + fuzzedCriteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } @@ -1402,30 +1385,26 @@ contract ProposalValidator_SubmitCouncilMemberElectionsProposal_TestFail is Prop vm.expectRevert(ProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, revocableAttestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, revocableAttestationUid, CYCLE_NUMBER ); } function test_submitCouncilMemberElectionsProposal_invalidVotingModule_reverts() public { - attestationUid = - _createApprovedProposerAttestation(topDelegate_A, ProposalValidator.ProposalType.CouncilMemberElections); - // Mock configurator to return uninitialized module _mockProposalTypesConfiguratorCallWithUninitializedModule(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); vm.prank(topDelegate_A); validator.submitCouncilMemberElectionsProposal( - criteriaValue, optionDescriptions, proposalDescription, attestationUid, CYCLE_NUMBER + criteriaValue, optionDescriptions, PROPOSAL_DESCRIPTION, approvedProposerAttestationUid, CYCLE_NUMBER ); } } /// @title ProposalValidator_SubmitFundingProposal_Test /// @notice Happy path tests for submitFundingProposal function -contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init { - uint128 criteriaValue = 1000 ether; - string description = "Test funding proposal"; +contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_TestInit { + uint128 constant FUNDING_CRITERIA_VALUE = 1000; function setUp() public override { super.setUp(); @@ -1435,39 +1414,40 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init } function testFuzz_submitFundingProposal_succeeds( - uint8 proposalTypeValue, - uint8 optionCount, - uint256 amount, - address proposer + uint8 fuzzedProposalTypeValue, + uint8 fuzzedOptionCount, + uint256 fuzzedAmount, + address fuzzedProposer ) public { // Assume proposer is not zero address - vm.assume(proposer != address(0)); + vm.assume(fuzzedProposer != address(0)); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Bound option count between 1 and 5 for reasonable test execution - optionCount = uint8(bound(optionCount, 1, 5)); + fuzzedOptionCount = uint8(bound(fuzzedOptionCount, 1, 5)); - // Bound amount from 0 to DISTRIBUTION_THRESHOLD (inclusive) - amount = bound(amount, 0, DISTRIBUTION_THRESHOLD); + // Bound amount from 0 to PROPOSAL_DISTRIBUTION_THRESHOLD (inclusive) + fuzzedAmount = bound(fuzzedAmount, 0, PROPOSAL_DISTRIBUTION_THRESHOLD); // Start with minimal arrays and extend based on option count (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = - _createMinimalFundingArrays(optionCount); + _createMinimalFundingArrays(fuzzedOptionCount); // fuzz the amounts - for (uint256 i = 0; i < optionCount; i++) { - amounts[i] = amount; + for (uint256 i = 0; i < fuzzedOptionCount; i++) { + amounts[i] = fuzzedAmount; } // Calculate expected proposal ID bytes memory votingModuleData = - _constructFundingVotingModuleData(descriptions, recipients, amounts, criteriaValue); - uint256 expectedId = - validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); + uint256 expectedId = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) + ); // Mock proposalSnapshot to return 0 (proposal doesn't exist in governor) _mockAndExpect( @@ -1476,7 +1456,7 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init // Expect ProposalSubmitted event vm.expectEmit(address(validator)); - emit ProposalSubmitted(expectedId, proposer, description, proposalType); + emit ProposalSubmitted(expectedId, fuzzedProposer, PROPOSAL_DESCRIPTION, proposalType); // Expect ProposalVotingModuleData event vm.expectEmit(address(validator)); @@ -1484,9 +1464,9 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); - vm.prank(proposer); + vm.prank(fuzzedProposer); uint256 proposalId = validator.submitFundingProposal( - criteriaValue, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); assertEq(proposalId, expectedId); @@ -1500,31 +1480,17 @@ contract ProposalValidator_SubmitFundingProposal_Test is ProposalValidator_Init uint256 votingCycle ) = validator.getProposalData(proposalId); - assertEq(storedProposer, proposer, "Proposer should match input"); + assertEq(storedProposer, fuzzedProposer, "Proposer should match input"); assertEq(uint8(storedProposalType), uint8(proposalType), "Proposal type should match input"); assertFalse(movedToVote, "Proposal should not be in voting yet"); assertEq(approvalCount, 0, "Approval count should be 0"); assertEq(votingCycle, CYCLE_NUMBER, "Voting cycle should match input"); } -} - -/// @title ProposalValidator_SubmitFundingProposal_TestFail -/// @notice Sad path tests for submitFundingProposal function -contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_Init { - uint128 public constant FUNDING_CRITERIA_VALUE = 50; - string description = "Test funding proposal"; - - function setUp() public override { - super.setUp(); - // Set both funding proposal types to use the approval voting module - _setGovernanceFundProposalType(); - _setCouncilBudgetProposalType(); - } - function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitFundingProposal_invalidProposalType_reverts(uint8 fuzzedProposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 2)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); @@ -1532,19 +1498,19 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } function testFuzz_submitFundingProposal_invalidVotingCycle_reverts( - uint8 proposalTypeValue, - uint256 votingCycle + uint8 fuzzedProposalTypeValue, + uint256 fuzzedVotingCycle ) public { - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); - vm.assume(votingCycle != CYCLE_NUMBER); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); + vm.assume(fuzzedVotingCycle != CYCLE_NUMBER); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); @@ -1552,14 +1518,20 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, votingCycle + FUNDING_CRITERIA_VALUE, + descriptions, + recipients, + amounts, + PROPOSAL_DESCRIPTION, + proposalType, + fuzzedVotingCycle ); } function testFuzz_submitFundingProposal_mismatchedDescriptionsLength_reverts( uint8 matchingLength, uint8 mismatchedLength, - uint8 proposalTypeValue + uint8 fuzzedProposalTypeValue ) public { @@ -1569,8 +1541,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.assume(matchingLength != mismatchedLength); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create arrays - recipients and amounts match, descriptions are different string[] memory mismatchedDescriptions = new string[](mismatchedLength); @@ -1584,7 +1556,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I mismatchedDescriptions, matchingRecipients, matchingAmounts, - description, + PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); @@ -1593,7 +1565,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I function testFuzz_submitFundingProposal_mismatchedRecipientsLength_reverts( uint8 matchingLength, uint8 mismatchedLength, - uint8 proposalTypeValue + uint8 fuzzedProposalTypeValue ) public { @@ -1603,8 +1575,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.assume(matchingLength != mismatchedLength); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create arrays - descriptions and amounts match, recipients are different string[] memory matchingDescriptions = new string[](matchingLength); @@ -1618,7 +1590,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingDescriptions, mismatchedRecipients, matchingAmounts, - description, + PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); @@ -1627,7 +1599,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I function testFuzz_submitFundingProposal_mismatchedAmountsLength_reverts( uint8 matchingLength, uint8 mismatchedLength, - uint8 proposalTypeValue + uint8 fuzzedProposalTypeValue ) public { @@ -1637,8 +1609,8 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.assume(matchingLength != mismatchedLength); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create arrays - descriptions and recipients match, amounts are different string[] memory matchingDescriptions = new string[](matchingLength); @@ -1652,41 +1624,41 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I matchingDescriptions, matchingRecipients, mismatchedAmounts, - description, + PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } function testFuzz_submitFundingProposal_exceedsProposalDistributionThreshold_reverts( - uint256 excessAmount, - uint8 proposalTypeValue + uint256 fuzzedExcessAmount, + uint8 fuzzedProposalTypeValue ) public { - // Bound excess amount to be greater than DISTRIBUTION_THRESHOLD - excessAmount = bound(excessAmount, DISTRIBUTION_THRESHOLD + 1, type(uint128).max); + // Bound excess amount to be greater than PROPOSAL_DISTRIBUTION_THRESHOLD + fuzzedExcessAmount = bound(fuzzedExcessAmount, PROPOSAL_DISTRIBUTION_THRESHOLD + 1, type(uint128).max); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create arrays with excessive amount (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); - amounts[0] = excessAmount; + amounts[0] = fuzzedExcessAmount; vm.expectRevert(ProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitFundingProposal_duplicateProposal_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitFundingProposal_duplicateProposal_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); @@ -1694,8 +1666,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - uint256 expectedId = - validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + uint256 expectedId = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) + ); // Mock proposalSnapshot to return 0 for first submission _mockAndExpect( @@ -1707,7 +1680,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Submit first proposal vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); // Attempt to submit identical proposal @@ -1717,14 +1690,14 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitFundingProposal_proposalExistsInGovernor_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitFundingProposal_proposalExistsInGovernor_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); @@ -1732,8 +1705,9 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I // Calculate expected proposal ID bytes memory votingModuleData = _constructFundingVotingModuleData(descriptions, recipients, amounts, FUNDING_CRITERIA_VALUE); - uint256 expectedId = - validator.hashProposalWithModule(approvalVotingModule, votingModuleData, keccak256(bytes(description))); + uint256 expectedId = validator.hashProposalWithModule( + approvalVotingModule, votingModuleData, keccak256(bytes(PROPOSAL_DESCRIPTION)) + ); // Mock proposalSnapshot to return non-zero (proposal already exists in governor) _mockAndExpect( @@ -1748,14 +1722,14 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } - function testFuzz_submitFundingProposal_zeroOptionsLength_reverts(uint8 proposalTypeValue) public { + function testFuzz_submitFundingProposal_zeroOptionsLength_reverts(uint8 fuzzedProposalTypeValue) public { // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); string[] memory emptyDescriptions = new string[](0); address[] memory emptyRecipients = new address[](0); uint256[] memory emptyAmounts = new uint256[](0); @@ -1767,26 +1741,26 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I emptyDescriptions, emptyRecipients, emptyAmounts, - description, + PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } function testFuzz_submitFundingProposal_exceedsMaxOptionsLength_reverts( - uint256 tooManyOptions, - uint8 proposalTypeValue + uint256 fuzzedTooManyOptions, + uint8 fuzzedProposalTypeValue ) public { // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Create arrays with more than 255 options (exceeds allowed uint8 max) - tooManyOptions = uint256(bound(tooManyOptions, 256, 512)); - string[] memory tooManyDescriptions = new string[](tooManyOptions); - address[] memory tooManyRecipients = new address[](tooManyOptions); - uint256[] memory tooManyAmounts = new uint256[](tooManyOptions); + fuzzedTooManyOptions = uint256(bound(fuzzedTooManyOptions, 256, 512)); + string[] memory tooManyDescriptions = new string[](fuzzedTooManyOptions); + address[] memory tooManyRecipients = new address[](fuzzedTooManyOptions); + uint256[] memory tooManyAmounts = new uint256[](fuzzedTooManyOptions); vm.expectRevert(ProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(user); @@ -1795,7 +1769,7 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I tooManyDescriptions, tooManyRecipients, tooManyAmounts, - description, + PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); @@ -1812,15 +1786,20 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.expectRevert(ProposalValidator.ProposalValidator_InvalidVotingModule.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } - function test_submitFundingProposal_invalidTotalBudget_reverts(uint8 proposalTypeValue, uint256 _amount) public { - _amount = bound(_amount, type(uint136).max, type(uint192).max); + function test_submitFundingProposal_invalidTotalBudget_reverts( + uint8 fuzzedProposalTypeValue, + uint256 fuzzedAmount + ) + public + { + fuzzedAmount = bound(fuzzedAmount, type(uint136).max, type(uint192).max); // Bound proposal type to only GovernanceFund (3) or CouncilBudget (4) - proposalTypeValue = uint8(bound(proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); (string[] memory descriptions, address[] memory recipients, uint256[] memory amounts) = _createMinimalFundingArrays(1); @@ -1828,43 +1807,45 @@ contract ProposalValidator_SubmitFundingProposal_TestFail is ProposalValidator_I vm.prank(owner); validator.setProposalDistributionThreshold(type(uint256).max); - amounts[0] = _amount; + amounts[0] = fuzzedAmount; vm.expectRevert(ProposalValidator.ProposalValidator_InvalidTotalBudget.selector); vm.prank(user); validator.submitFundingProposal( - FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, description, proposalType, CYCLE_NUMBER + FUNDING_CRITERIA_VALUE, descriptions, recipients, amounts, PROPOSAL_DESCRIPTION, proposalType, CYCLE_NUMBER ); } } /// @title ProposalValidator_ApproveProposal_Test /// @notice Happy path tests for approveProposal function -contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { +contract ProposalValidator_ApproveProposal_Test is ProposalValidator_TestInit { function setUp() public override { super.setUp(); // create the previous voting cycle // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); + validator.setVotingCycleData( + CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, VOTING_CYCLE_DISTRIBUTION_LIMIT + ); // warp to the start of the previous cycle vm.warp(START_TIMESTAMP - DURATION); } - function test_approveProposal_succeeds(uint256 _proposalId, uint8 proposalTypeValue) public { + function test_approveProposal_succeeds(uint256 fuzzedProposalId, uint8 fuzzedProposalTypeValue) public { // Ensure the proposal ID is not 0 - vm.assume(_proposalId != 0); + vm.assume(fuzzedProposalId != 0); // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Expect event to be emitted when approving vm.expectEmit(address(validator)); - emit ProposalApproved(_proposalId, topDelegate_A); + emit ProposalApproved(fuzzedProposalId, topDelegate_A); // warp to the start of current cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { @@ -1873,109 +1854,94 @@ contract ProposalValidator_ApproveProposal_Test is ProposalValidator_Init { // Approve the proposal, use the attestation of the top delegate that was created in setUp vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); // Check that the proposal data has been updated - assertTrue(validator.hasDelegateApproved(_proposalId, topDelegate_A)); + assertTrue(validator.hasDelegateApproved(fuzzedProposalId, topDelegate_A)); - (,,, uint256 approvalCount,) = validator.getProposalData(_proposalId); + (,,, uint256 approvalCount,) = validator.getProposalData(fuzzedProposalId); assertEq(approvalCount, 1); } -} -/// @title ProposalValidator_ApproveProposal_TestFail -/// @notice Sad path tests for approveProposal function -contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { - function setUp() public override { - super.setUp(); - - // create the previous voting cycle - // cycle number decreased by 1 and start time CYCLE_DURATION before the current cycle - vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER - 1, START_TIMESTAMP - DURATION, DURATION, DISTRIBUTION_THRESHOLD); - // warp to the start of the previous cycle - vm.warp(START_TIMESTAMP - DURATION); - } - - function test_approveProposal_proposalDoesNotExist_reverts(uint256 _proposalId) public { + function test_approveProposal_proposalDoesNotExist_reverts(uint256 fuzzedProposalId) public { // There is no stored proposal data so this will revert vm.expectRevert(IProposalValidator.ProposalValidator_ProposalDoesNotExist.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyApproved_reverts( - uint256 _proposalId, - uint8 proposalTypeValue + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // Mock the proposal as already approved by the top delegate - validator.mockApproveProposal(_proposalId, topDelegate_A); + validator.mockApproveProposal(fuzzedProposalId, topDelegate_A); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyApproved.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_proposalAlreadyMovedToVote_reverts( - uint256 _proposalId, - uint8 proposalTypeValue + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // set proposal data so that the proposal exists and set movedToVote to true - validator.setProposalData(_proposalId, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, true, 0, CYCLE_NUMBER); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_previousVotingCycleNotStarted_reverts( - uint256 _proposalId, - uint8 proposalTypeValue + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp before the start of the previous cycle so that it reverts vm.warp(START_TIMESTAMP - DURATION - 1); vm.expectRevert(IProposalValidator.ProposalValidator_PreviousVotingCycleNotStarted.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_invalidVotingCycle_reverts( - uint256 _proposalId, - uint8 proposalTypeValue, - uint256 votingCycle + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue, + uint256 fuzzedVotingCycle ) public { - vm.assume(votingCycle != CYCLE_NUMBER && votingCycle != 0); - vm.assume(votingCycle != CYCLE_NUMBER + 1); // Avoid existing cycle + vm.assume(fuzzedVotingCycle != CYCLE_NUMBER && fuzzedVotingCycle != 0); + vm.assume(fuzzedVotingCycle != CYCLE_NUMBER + 1); // Avoid existing cycle // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, votingCycle); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, fuzzedVotingCycle); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { @@ -1984,13 +1950,18 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } - function test_approveProposal_invalidSchema_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { + function test_approveProposal_invalidSchema_reverts( + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue + ) + public + { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // create a new schema vm.prank(topDelegate_A); @@ -2015,7 +1986,7 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { ); // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { vm.warp(START_TIMESTAMP); @@ -2023,15 +1994,20 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestationSchema.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, _invalidAttestationUid); + validator.approveProposal(fuzzedProposalId, _invalidAttestationUid); } - function test_approveProposal_attestationRevoked_reverts(uint256 _proposalId, uint8 proposalTypeValue) public { + function test_approveProposal_attestationRevoked_reverts( + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue + ) + public + { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { vm.warp(START_TIMESTAMP); @@ -2048,18 +2024,18 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { vm.expectRevert(IProposalValidator.ProposalValidator_AttestationRevoked.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_attestationCreatedAfterPreviousVotingCycle_reverts( - uint256 _proposalId, - uint8 proposalTypeValue + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // create a new delegate and attestation address _delegate = makeAddr("delegate"); @@ -2078,31 +2054,31 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { } // set proposal data so that the proposal exists - validator.setProposalData(_proposalId, _delegate, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, _delegate, proposalType, false, 0, CYCLE_NUMBER); // warp to after the start of the current voting cycle vm.warp(START_TIMESTAMP + 2); vm.expectRevert(IProposalValidator.ProposalValidator_AttestationCreatedAfterLastVotingCycle.selector); vm.prank(_delegate); - validator.approveProposal(_proposalId, _attestationUid); + validator.approveProposal(fuzzedProposalId, _attestationUid); } function test_approveProposal_invalidAttestationCaller_reverts( - uint256 _proposalId, - uint8 proposalTypeValue, - address _caller + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue, + address fuzzedCaller ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Ensure the caller is not a top delegate - vm.assume(_caller != topDelegate_A); + vm.assume(fuzzedCaller != topDelegate_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { vm.warp(START_TIMESTAMP); @@ -2110,22 +2086,22 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); - vm.prank(_caller); - validator.approveProposal(_proposalId, topDelegateAttestation_A); + vm.prank(fuzzedCaller); + validator.approveProposal(fuzzedProposalId, topDelegateAttestation_A); } function test_approveProposal_invalidAttestationPartialDelegation_reverts( - uint256 _proposalId, - uint8 proposalTypeValue + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { vm.warp(START_TIMESTAMP); @@ -2150,25 +2126,25 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, _attestationUidWithPartialDelegation); + validator.approveProposal(fuzzedProposalId, _attestationUidWithPartialDelegation); } function test_approveProposal_nonExistentAttestation_reverts( - uint256 _proposalId, - uint8 proposalTypeValue, - bytes32 _nonExistentAttestationUid + uint256 fuzzedProposalId, + uint8 fuzzedProposalTypeValue, + bytes32 fuzzedNonExistentAttestationUid ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Ensure the attestation uid is not one of the valid ones - vm.assume(_nonExistentAttestationUid != topDelegateAttestation_A); + vm.assume(fuzzedNonExistentAttestationUid != topDelegateAttestation_A); // Set mock proposal data of a random proposal in the validator contract - validator.setProposalData(_proposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); + validator.setProposalData(fuzzedProposalId, topDelegate_A, proposalType, false, 0, CYCLE_NUMBER); // warp to start of current voting cycle if the proposal is ProtocolOrGovernorUpgrade if (proposalType == ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade) { vm.warp(START_TIMESTAMP); @@ -2177,15 +2153,13 @@ contract ProposalValidator_ApproveProposal_TestFail is ProposalValidator_Init { // Expect the invalid attestation error to be reverted when attestation doesn't exist vm.expectRevert(IProposalValidator.ProposalValidator_InvalidAttestation.selector); vm.prank(topDelegate_A); - validator.approveProposal(_proposalId, _nonExistentAttestationUid); + validator.approveProposal(fuzzedProposalId, fuzzedNonExistentAttestationUid); } } /// @title ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test /// @notice Happy path tests for moveToVoteProtocolOrGovernorUpgradeProposal function -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; +contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is ProposalValidator_TestInit { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; bytes votingModuleData; uint256 expectedId; @@ -2194,7 +2168,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P super.setUp(); (expectedId, votingModuleData) = - _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); + _createUpgradeProposalForMoveToVote(approvedProposer, AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); } function test_moveToVoteProtocolOrGovernorUpgradeProposal_succeeds() public { @@ -2206,7 +2180,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) + (optimisticVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, OPTIMISTIC_VOTING_MODULE_ID) ), abi.encode(expectedId) ); @@ -2217,51 +2191,36 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_Test is P // Move to vote vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); // Check that the proposal is in voting (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } -} - -contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail is ProposalValidator_Init { - uint248 againstThreshold = 5000; // 50% - string proposalDescription = "Test proposal"; - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - bytes votingModuleData; - uint256 expectedId; - - function setUp() public override { - super.setUp(); - - (expectedId, votingModuleData) = - _createUpgradeProposalForMoveToVote(approvedProposer, againstThreshold, proposalDescription); - } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address _caller) public { - vm.assume(_caller != approvedProposer); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposer_reverts(address fuzzedCaller) public { + vm.assume(fuzzedCaller != approvedProposer); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + vm.prank(fuzzedCaller); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 _againstThreshold) + function test_moveToVoteProtocolOrGovernorUpgradeProposal_invalidProposal_reverts(uint248 fuzzedAgainstThreshold) public { // This will generate a different proposal ID which will make the proposal type wrong - vm.assume(_againstThreshold != againstThreshold); + vm.assume(fuzzedAgainstThreshold != AGAINST_THRESHOLD); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(_againstThreshold, proposalDescription); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(fuzzedAgainstThreshold, PROPOSAL_DESCRIPTION); } function test_moveToVoteProtocolOrGovernorUpgradeProposal_insufficientApprovals_reverts() public { @@ -2273,7 +2232,7 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); } function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalAlreadyMovedToVote_reverts() public { @@ -2285,11 +2244,13 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); } - function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(uint256 _randomId) public { - vm.assume(_randomId != expectedId); + function test_moveToVoteProtocolOrGovernorUpgradeProposal_proposalIdMismatch_reverts(uint256 fuzzedProposalId) + public + { + vm.assume(fuzzedProposalId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(OPTIMISTIC_VOTING_MODULE_ID); @@ -2299,23 +2260,22 @@ contract ProposalValidator_MoveToVoteProtocolOrGovernorUpgradeProposal_TestFail address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (optimisticVotingModule, votingModuleData, proposalDescription, OPTIMISTIC_VOTING_MODULE_ID) + (optimisticVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, OPTIMISTIC_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomId)) + abi.encode(uint256(fuzzedProposalId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); vm.prank(approvedProposer); - validator.moveToVoteProtocolOrGovernorUpgradeProposal(againstThreshold, proposalDescription); + validator.moveToVoteProtocolOrGovernorUpgradeProposal(AGAINST_THRESHOLD, PROPOSAL_DESCRIPTION); } } -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_Init { +contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is ProposalValidator_TestInit { ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; uint128 criteriaValue = 1; uint256 expectedId; bytes votingModuleData; - string proposalDescription = "Test proposal"; string[] optionsDescriptions = new string[](2); function setUp() public override { @@ -2325,7 +2285,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop optionsDescriptions[0] = "Option 1"; optionsDescriptions[1] = "Option 2"; (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, criteriaValue, optionsDescriptions, proposalDescription + approvedProposer, criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION ); } @@ -2338,7 +2298,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) + (approvalVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, APPROVAL_VOTING_MODULE_ID) ), abi.encode(expectedId) ); @@ -2350,42 +2310,22 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_Test is Prop // Move to vote vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); // Check that the proposal is in voting (,, bool movedToVote,,) = validator.getProposalData(expectedId); assertTrue(movedToVote, "Proposal should be in voting"); } -} - -contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType.CouncilMemberElections; - uint128 criteriaValue = 1; - string proposalDescription = "Test proposal"; - string[] optionsDescriptions = new string[](2); - uint256 expectedId; - bytes votingModuleData; - - function setUp() public override { - super.setUp(); - - // Create a proposal for move to vote with 1 top choice and 2 options - optionsDescriptions[0] = "Option 1"; - optionsDescriptions[1] = "Option 2"; - (expectedId, votingModuleData) = _createCouncilElectionProposalForMoveToVote( - approvedProposer, criteriaValue, optionsDescriptions, proposalDescription - ); - } - function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address _caller) public { - vm.assume(_caller != approvedProposer); + function test_moveToVoteCouncilMemberElectionsProposal_invalidProposer_reverts(address fuzzedCaller) public { + vm.assume(fuzzedCaller != approvedProposer); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposer.selector); - vm.prank(_caller); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + vm.prank(fuzzedCaller); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } function test_moveToVoteCouncilMemberElectionsProposal_invalidProposal_reverts() public { @@ -2397,17 +2337,17 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(_criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } function test_moveToVoteCouncilMemberElectionsProposal_invalidOptionsLength_reverts() public { vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](0), proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](0), PROPOSAL_DESCRIPTION); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](256), proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, new string[](256), PROPOSAL_DESCRIPTION); } function test_moveToVoteCouncilMemberElectionsProposal_insufficientApprovals_reverts() public { @@ -2419,7 +2359,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } function test_moveToVoteCouncilMemberElectionsProposal_proposalAlreadyMovedToVote_reverts() public { @@ -2431,7 +2371,7 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } function test_moveToVoteCouncilMemberElectionsProposal_invalidVotingCycle_reverts() public { @@ -2441,11 +2381,13 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is vm.expectRevert(IProposalValidator.ProposalValidator_InvalidVotingCycle.selector); vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } - function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(uint256 _randomId) public { - vm.assume(_randomId != expectedId); + function test_moveToVoteCouncilMemberElectionsProposal_proposalIdMismatch_reverts(uint256 fuzzedProposalId) + public + { + vm.assume(fuzzedProposalId != expectedId); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); @@ -2455,19 +2397,19 @@ contract ProposalValidator_MoveToVoteCouncilMemberElectionsProposal_TestFail is address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) + (approvalVotingModule, votingModuleData, PROPOSAL_DESCRIPTION, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomId)) + abi.encode(uint256(fuzzedProposalId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); - validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, proposalDescription); + validator.moveToVoteCouncilMemberElectionsProposal(criteriaValue, optionsDescriptions, PROPOSAL_DESCRIPTION); } } -contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_Init { +contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_TestInit { ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; uint128 criteriaValue = 1; @@ -2592,82 +2534,48 @@ contract ProposalValidator_MoveToVoteFundingProposal_Test is ProposalValidator_I councilBudgetProposalType ); } -} - -contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidator_Init { - ProposalValidator.ProposalType governanceFundProposalType = ProposalValidator.ProposalType.GovernanceFund; - ProposalValidator.ProposalType councilBudgetProposalType = ProposalValidator.ProposalType.CouncilBudget; - uint128 criteriaValue = 1; - string governanceFundProposalDescription = "Test governance fund proposal"; - string councilBudgetProposalDescription = "Test council budget proposal"; - string[] optionsDescriptions; - address[] optionsRecipients; - uint256[] optionsAmounts; - uint256 governanceFundExpectedId; - uint256 councilBudgetExpectedId; - bytes governanceFundVotingModuleData; - bytes councilBudgetVotingModuleData; - - function setUp() public override { - super.setUp(); - - (optionsDescriptions, optionsRecipients, optionsAmounts) = _createMinimalFundingArrays(1); - (governanceFundExpectedId, governanceFundVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - governanceFundProposalDescription, - governanceFundProposalType - ); - (councilBudgetExpectedId, councilBudgetVotingModuleData) = _createFundingProposalForMoveToVote( - approvedProposer, - criteriaValue, - optionsDescriptions, - optionsRecipients, - optionsAmounts, - councilBudgetProposalDescription, - councilBudgetProposalType - ); - } function test_moveToVoteFundingProposal_invalidFundingProposalType_reverts( - uint8 _proposalTypeValue, - string memory _proposalDescription + uint8 fuzzedProposalTypeValue, + string memory fuzzedProposalDescription ) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 0, 2)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidFundingProposalType.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, _proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fuzzedProposalDescription, + proposalType ); } function test_moveToVoteFundingProposal_invalidProposal_reverts( - uint8 _proposalTypeValue, - uint128 _criteriaValue + uint8 fuzzedProposalTypeValue, + uint128 fuzzedCriteriaValue ) public { // Ensure the criteria value is not the same as the one in setUp so when calculating the proposal ID it will // not find the proposal - vm.assume(_criteriaValue != criteriaValue); + vm.assume(fuzzedCriteriaValue != criteriaValue); // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2676,36 +2584,41 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.expectRevert(IProposalValidator.ProposalValidator_InvalidProposal.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - _criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + fuzzedCriteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } function test_moveToVoteFundingProposal_invalidProposalWrongProposalType_reverts( - uint8 _wrongProposalTypeValue, - uint8 _validProposalTypeValue + uint8 fuzzedWrongProposalTypeValue, + uint8 fuzzedValidProposalTypeValue ) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _wrongProposalTypeValue = uint8(bound(_wrongProposalTypeValue, 0, 2)); - ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(_wrongProposalTypeValue); + fuzzedWrongProposalTypeValue = uint8(bound(fuzzedWrongProposalTypeValue, 0, 2)); + ProposalValidator.ProposalType wrongProposalType = ProposalValidator.ProposalType(fuzzedWrongProposalTypeValue); - _validProposalTypeValue = uint8(bound(_validProposalTypeValue, 3, 4)); - ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(_validProposalTypeValue); + fuzzedValidProposalTypeValue = uint8(bound(fuzzedValidProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType validProposalType = ProposalValidator.ProposalType(fuzzedValidProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (validProposalType == governanceFundProposalType) { // Set proposal data proposal type to a different value validator.setProposalData( - governanceFundExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + expectedGovernanceFundId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { // Set proposal data proposal type to a different value validator.setProposalData( - councilBudgetExpectedId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER + expectedCouncilBudgetId, approvedProposer, wrongProposalType, false, 0, CYCLE_NUMBER ); - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2718,79 +2631,89 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat optionsDescriptions, optionsRecipients, optionsAmounts, - proposalDescription, + fundingProposalDescription, validProposalType ); } - function test_moveToVoteFundingProposal_invalidOptionsLength_reverts(uint8 _proposalTypeValue) public { + function test_moveToVoteFundingProposal_invalidOptionsLength_reverts(uint8 fuzzedProposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, new string[](0), optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, new string[](0), optionsRecipients, optionsAmounts, fundingProposalDescription, proposalType ); vm.expectRevert(IProposalValidator.ProposalValidator_InvalidOptionsLength.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, new string[](256), optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + new string[](256), + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } function test_moveToVoteFundingProposal_invalidTotalBudget_reverts( - uint8 _proposalTypeValue, - uint256 _amount + uint8 fuzzedProposalTypeValue, + uint256 fuzzedAmount ) public { - _amount = bound(_amount, type(uint136).max, type(uint192).max); + fuzzedAmount = bound(fuzzedAmount, type(uint136).max, type(uint192).max); // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } vm.prank(owner); validator.setProposalDistributionThreshold(type(uint256).max); - optionsAmounts[0] = _amount; + optionsAmounts[0] = fuzzedAmount; vm.expectRevert(IProposalValidator.ProposalValidator_InvalidTotalBudget.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } - function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 _proposalTypeValue) public { + function test_moveToVoteFundingProposal_insufficientApprovals_reverts(uint8 fuzzedProposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; + validator.setProposalData(expectedGovernanceFundId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + fundingProposalDescription = governanceFundProposalDescription; } else { // Set proposal data approved count to 0 since it is 1 by the approval on the setUp - validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; + validator.setProposalData(expectedCouncilBudgetId, approvedProposer, proposalType, false, 0, CYCLE_NUMBER); + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2799,24 +2722,29 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.expectRevert(IProposalValidator.ProposalValidator_InsufficientApprovals.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } - function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 _proposalTypeValue) public { + function test_moveToVoteFundingProposal_proposalAlreadyMovedToVote_reverts(uint8 fuzzedProposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { // Set proposal data movedToVote to true - validator.setProposalData(governanceFundExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = governanceFundProposalDescription; + validator.setProposalData(expectedGovernanceFundId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + fundingProposalDescription = governanceFundProposalDescription; } else { // Set proposal data movedToVote to true - validator.setProposalData(councilBudgetExpectedId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); - proposalDescription = councilBudgetProposalDescription; + validator.setProposalData(expectedCouncilBudgetId, approvedProposer, proposalType, true, 1, CYCLE_NUMBER); + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2825,20 +2753,25 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.expectRevert(IProposalValidator.ProposalValidator_ProposalAlreadyMovedToVote.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } - function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 _proposalTypeValue) public { + function test_moveToVoteFundingProposal_invalidVotingCycle_reverts(uint8 fuzzedProposalTypeValue) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2848,46 +2781,58 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.warp(START_TIMESTAMP + DURATION + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } function test_moveToVoteFundingProposal_buildApprovalModuleOptionsExceedsProposalDistributionThreshold_reverts( - uint8 _proposalTypeValue + uint8 fuzzedProposalTypeValue ) public { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Set the first option amount to exceed the distribution threshold - optionsAmounts[0] = DISTRIBUTION_THRESHOLD + 1; + optionsAmounts[0] = PROPOSAL_DISTRIBUTION_THRESHOLD + 1; vm.expectRevert(IProposalValidator.ProposalValidator_ExceedsDistributionThreshold.selector); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } - function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 _proposalTypeValue) public { + function test_moveToVoteFundingProposal_exceedsDistributionThreshold_reverts(uint8 fuzzedProposalTypeValue) + public + { // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the proposal types configurator call @@ -2905,9 +2850,9 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat _optionsRecipients[1] = makeAddr("optionRecipient2"); _optionsRecipients[2] = makeAddr("optionRecipient3"); - _optionsAmounts[0] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[1] = DISTRIBUTION_THRESHOLD - 1; - _optionsAmounts[2] = DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[0] = PROPOSAL_DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[1] = PROPOSAL_DISTRIBUTION_THRESHOLD - 1; + _optionsAmounts[2] = PROPOSAL_DISTRIBUTION_THRESHOLD - 1; _createFundingProposalForMoveToVote( approvedProposer, @@ -2915,7 +2860,7 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat _optionsDescriptions, _optionsRecipients, _optionsAmounts, - proposalDescription, + fundingProposalDescription, proposalType ); @@ -2923,33 +2868,38 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat vm.prank(approvedProposer); vm.warp(START_TIMESTAMP + 1); validator.moveToVoteFundingProposal( - criteriaValue, _optionsDescriptions, _optionsRecipients, _optionsAmounts, proposalDescription, proposalType + criteriaValue, + _optionsDescriptions, + _optionsRecipients, + _optionsAmounts, + fundingProposalDescription, + proposalType ); } function test_moveToVoteFundingProposal_proposalIdMismatch_reverts( - uint8 _proposalTypeValue, - uint256 _randomId + uint8 fuzzedProposalTypeValue, + uint256 fuzzedProposalId ) public { - vm.assume(_randomId != governanceFundExpectedId && _randomId != councilBudgetExpectedId); + vm.assume(fuzzedProposalId != expectedGovernanceFundId && fuzzedProposalId != expectedCouncilBudgetId); // Valid funding proposal types are GovernanceFund (3) and CouncilBudget (4) - _proposalTypeValue = uint8(bound(_proposalTypeValue, 3, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(_proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 3, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); // Mock the proposal types configurator call _mockProposalTypesConfiguratorCall(APPROVAL_VOTING_MODULE_ID); bytes memory votingModuleData; - string memory proposalDescription; + string memory fundingProposalDescription; if (proposalType == governanceFundProposalType) { votingModuleData = governanceFundVotingModuleData; - proposalDescription = governanceFundProposalDescription; + fundingProposalDescription = governanceFundProposalDescription; } else { votingModuleData = councilBudgetVotingModuleData; - proposalDescription = councilBudgetProposalDescription; + fundingProposalDescription = councilBudgetProposalDescription; } // Mock the governor.proposeWithModule call @@ -2957,135 +2907,159 @@ contract ProposalValidator_MoveToVoteFundingProposal_TestFail is ProposalValidat address(governor), abi.encodeCall( IOptimismGovernor.proposeWithModule, - (approvalVotingModule, votingModuleData, proposalDescription, APPROVAL_VOTING_MODULE_ID) + (approvalVotingModule, votingModuleData, fundingProposalDescription, APPROVAL_VOTING_MODULE_ID) ), - abi.encode(uint256(_randomId)) + abi.encode(uint256(fuzzedProposalId)) ); vm.expectRevert(IProposalValidator.ProposalValidator_ProposalIdMismatch.selector); vm.warp(START_TIMESTAMP + 1); vm.prank(approvedProposer); validator.moveToVoteFundingProposal( - criteriaValue, optionsDescriptions, optionsRecipients, optionsAmounts, proposalDescription, proposalType + criteriaValue, + optionsDescriptions, + optionsRecipients, + optionsAmounts, + fundingProposalDescription, + proposalType ); } } /// @title ProposalValidator_Setters_Test /// @notice Tests for setter functions -contract ProposalValidator_Setters_Test is ProposalValidator_Init { +contract ProposalValidator_SetVotingCycleData_Test is ProposalValidator_TestInit { function testFuzz_setVotingCycleData_succeeds( - uint256 cycleNumber, - uint256 startingTimestamp, - uint256 duration, - uint256 distributionLimit + uint256 fuzzedCycleNumber, + uint256 fuzzedStartingTimestamp, + uint256 fuzzedDuration, + uint256 fuzzedDistributionLimit ) public { - vm.assume(cycleNumber != CYCLE_NUMBER); // Avoid existing cycle + vm.assume(fuzzedCycleNumber != CYCLE_NUMBER); // Avoid existing cycle // Expect the VotingCycleDataSet event to be emitted vm.expectEmit(address(validator)); - emit VotingCycleDataSet(cycleNumber, startingTimestamp, duration, distributionLimit); + emit VotingCycleDataSet(fuzzedCycleNumber, fuzzedStartingTimestamp, fuzzedDuration, fuzzedDistributionLimit); vm.prank(owner); - validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); + validator.setVotingCycleData( + fuzzedCycleNumber, fuzzedStartingTimestamp, fuzzedDuration, fuzzedDistributionLimit + ); ( uint256 actualStartingTimestamp, uint256 actualDuration, uint256 actualDistributionLimit, uint256 actualMovedToVoteTokenCount - ) = validator.votingCycles(cycleNumber); + ) = validator.votingCycles(fuzzedCycleNumber); - assertEq(actualStartingTimestamp, startingTimestamp); - assertEq(actualDuration, duration); - assertEq(actualDistributionLimit, distributionLimit); + assertEq(actualStartingTimestamp, fuzzedStartingTimestamp); + assertEq(actualDuration, fuzzedDuration); + assertEq(actualDistributionLimit, fuzzedDistributionLimit); assertEq(actualMovedToVoteTokenCount, 0); } function testFuzz_setVotingCycleData_notOwner_reverts( - address caller, - uint256 cycleNumber, - uint256 startingTimestamp, - uint256 duration, - uint256 distributionLimit + address fuzzedCaller, + uint256 fuzzedCycleNumber, + uint256 fuzzedStartingTimestamp, + uint256 fuzzedDuration, + uint256 fuzzedDistributionLimit ) public { - vm.assume(caller != owner); + vm.assume(fuzzedCaller != owner); - vm.prank(caller); + vm.prank(fuzzedCaller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setVotingCycleData(cycleNumber, startingTimestamp, duration, distributionLimit); + validator.setVotingCycleData( + fuzzedCycleNumber, fuzzedStartingTimestamp, fuzzedDuration, fuzzedDistributionLimit + ); } function testFuzz_setVotingCycleData_votingCycleAlreadySet_reverts( - uint256 startingTimestamp, - uint256 duration, - uint256 distributionLimit + uint256 fuzzedStartingTimestamp, + uint256 fuzzedDuration, + uint256 fuzzedDistributionLimit ) public { vm.expectRevert(ProposalValidator.ProposalValidator_VotingCycleAlreadySet.selector); vm.prank(owner); - validator.setVotingCycleData(CYCLE_NUMBER, startingTimestamp, duration, distributionLimit); + validator.setVotingCycleData(CYCLE_NUMBER, fuzzedStartingTimestamp, fuzzedDuration, fuzzedDistributionLimit); } +} - function testFuzz_setProposalDistributionThreshold_succeeds(uint256 newProposalDistributionThreshold) public { +/// @title ProposalValidator_SetProposalDistributionThreshold_Test +/// @notice Tests for the setProposalDistributionThreshold function +contract ProposalValidator_SetProposalDistributionThreshold_Test is ProposalValidator_TestInit { + function testFuzz_setProposalDistributionThreshold_succeeds(uint256 fuzzedNewProposalDistributionThreshold) + public + { // Expect the ProposalDistributionThresholdSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalDistributionThresholdSet(newProposalDistributionThreshold); + emit ProposalDistributionThresholdSet(fuzzedNewProposalDistributionThreshold); vm.prank(owner); - validator.setProposalDistributionThreshold(newProposalDistributionThreshold); + validator.setProposalDistributionThreshold(fuzzedNewProposalDistributionThreshold); - assertEq(validator.proposalDistributionThreshold(), newProposalDistributionThreshold); + assertEq(validator.proposalDistributionThreshold(), fuzzedNewProposalDistributionThreshold); } - function testFuzz_setProposalDistributionThreshold_notOwner_reverts(address caller, uint256 threshold) public { - vm.assume(caller != owner); + function testFuzz_setProposalDistributionThreshold_notOwner_reverts( + address fuzzedCaller, + uint256 fuzzedThreshold + ) + public + { + vm.assume(fuzzedCaller != owner); - vm.prank(caller); + vm.prank(fuzzedCaller); vm.expectRevert("Ownable: caller is not the owner"); - validator.setProposalDistributionThreshold(threshold); + validator.setProposalDistributionThreshold(fuzzedThreshold); } +} +/// @title ProposalValidator_SetProposalTypeData_Test +/// @notice Tests for the setProposalTypeData function +contract ProposalValidator_SetProposalTypeData_Test is ProposalValidator_TestInit { function testFuzz_setProposalTypeData_succeeds( - uint8 proposalTypeValue, - uint256 newRequiredApprovals, - uint8 newProposalTypeId + uint8 fuzzedProposalTypeValue, + uint256 fuzzedNewRequiredApprovals, + uint8 fuzzedNewProposalTypeId ) public { // Bound the proposal type to valid enum values (0-4) - proposalTypeValue = uint8(bound(proposalTypeValue, 0, 4)); - ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(proposalTypeValue); + fuzzedProposalTypeValue = uint8(bound(fuzzedProposalTypeValue, 0, 4)); + ProposalValidator.ProposalType proposalType = ProposalValidator.ProposalType(fuzzedProposalTypeValue); ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ - requiredApprovals: newRequiredApprovals, - idInConfigurator: newProposalTypeId + requiredApprovals: fuzzedNewRequiredApprovals, + idInConfigurator: fuzzedNewProposalTypeId }); // Expect the ProposalTypeDataSet event to be emitted vm.expectEmit(address(validator)); - emit ProposalTypeDataSet(proposalType, newRequiredApprovals, newProposalTypeId); + emit ProposalTypeDataSet(proposalType, fuzzedNewRequiredApprovals, fuzzedNewProposalTypeId); vm.prank(owner); validator.setProposalTypeData(proposalType, newData); (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalType); - assertEq(requiredApprovals, newRequiredApprovals); - assertEq(idInConfigurator, newProposalTypeId); + assertEq(requiredApprovals, fuzzedNewRequiredApprovals); + assertEq(idInConfigurator, fuzzedNewProposalTypeId); } - function testFuzz_setProposalTypeData_notOwner_reverts(address caller) public { - vm.assume(caller != owner); + function testFuzz_setProposalTypeData_notOwner_reverts(address fuzzedCaller) public { + vm.assume(fuzzedCaller != owner); ProposalValidator.ProposalTypeData memory newData = ProposalValidator.ProposalTypeData({ requiredApprovals: 4, idInConfigurator: 0 }); - vm.prank(caller); + vm.prank(fuzzedCaller); vm.expectRevert("Ownable: caller is not the owner"); validator.setProposalTypeData(ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade, newData); } @@ -3093,18 +3067,21 @@ contract ProposalValidator_Setters_Test is ProposalValidator_Init { /// @title ProposalValidator_HashProposalWithModule_Test /// @notice Tests for the hashProposalWithModule function -contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_Init { +contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_TestInit { function testFuzz_hashProposalWithModule_succeeds( - address module, - bytes memory proposalData, - bytes32 descriptionHash + address fuzzedModule, + bytes memory fuzzedProposalData, + bytes32 fuzzedDescriptionHash ) public view { - uint256 id = validator.hashProposalWithModule(module, proposalData, descriptionHash); - uint256 expectedId = - uint256(keccak256(abi.encode(address(validator.GOVERNOR()), module, proposalData, descriptionHash))); + uint256 id = validator.hashProposalWithModule(fuzzedModule, fuzzedProposalData, fuzzedDescriptionHash); + uint256 expectedId = uint256( + keccak256( + abi.encode(address(validator.GOVERNOR()), fuzzedModule, fuzzedProposalData, fuzzedDescriptionHash) + ) + ); assertEq(id, expectedId); } From ddd45ee15b2e80f54c47e639f5c6592f036df1f2 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:49:06 +0300 Subject: [PATCH 72/73] fix: make validator immutable (#481) * fix: make validator immutable * refactor: add version to ProposalValidator --------- Co-authored-by: Flux <175354924+0xiamflux@users.noreply.github.com> --- .../governance/IProposalValidator.sol | 14 +-- .../snapshots/abi/ProposalValidator.json | 76 +----------- .../snapshots/semver-lock.json | 4 +- .../storageLayout/ProposalValidator.json | 38 +----- .../src/governance/ProposalValidator.sol | 44 ++----- .../test/governance/ProposalValidator.t.sol | 110 ++---------------- 6 files changed, 33 insertions(+), 253 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol index d09180e1c98..48a26114c51 100644 --- a/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol +++ b/packages/contracts-bedrock/interfaces/governance/IProposalValidator.sol @@ -2,13 +2,12 @@ pragma solidity ^0.8.0; // Interfaces -import {IOptimismGovernor} from './IOptimismGovernor.sol'; +import { IOptimismGovernor } from "./IOptimismGovernor.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title IProposalValidator /// @notice Interface for the ProposalValidator contract. interface IProposalValidator is ISemver { - error ReinitializableBase_ZeroInitVersion(); error ProposalValidator_InsufficientApprovals(); error ProposalValidator_ProposalAlreadyApproved(); error ProposalValidator_ProposalAlreadySubmitted(); @@ -72,8 +71,6 @@ interface IProposalValidator is ISemver { bytes encodedVotingModuleData ); - event Initialized(uint8 version); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); struct ProposalData { @@ -167,13 +164,6 @@ interface IProposalValidator is ISemver { ProposalTypeData memory _proposalTypeData ) external; - function initialize( - address _owner, - uint256 _proposalDistributionThreshold, - ProposalType[] memory _proposalTypes, - ProposalTypeData[] memory _proposalTypesData - ) external; - function renounceOwnership() external; function transferOwnership(address newOwner) external; @@ -184,7 +174,6 @@ interface IProposalValidator is ISemver { function owner() external view returns (address); - function initVersion() external view returns (uint8); function APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID() external view returns (bytes32); @@ -202,6 +191,7 @@ interface IProposalValidator is ISemver { ); function __constructor__( + address _owner, IOptimismGovernor _governor, bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid diff --git a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json index 68099896460..0dc60b6f839 100644 --- a/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/ProposalValidator.json @@ -1,6 +1,11 @@ [ { "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, { "internalType": "contract IOptimismGovernor", "name": "_governor", @@ -90,59 +95,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "initVersion", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_proposalDistributionThreshold", - "type": "uint256" - }, - { - "internalType": "enum ProposalValidator.ProposalType[]", - "name": "_proposalTypes", - "type": "uint8[]" - }, - { - "components": [ - { - "internalType": "uint256", - "name": "requiredApprovals", - "type": "uint256" - }, - { - "internalType": "uint8", - "name": "idInConfigurator", - "type": "uint8" - } - ], - "internalType": "struct ProposalValidator.ProposalTypeData[]", - "name": "_proposalTypesData", - "type": "tuple[]" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -555,19 +507,6 @@ "stateMutability": "view", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "Initialized", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -868,10 +807,5 @@ "inputs": [], "name": "ProposalValidator_VotingCycleAlreadySet", "type": "error" - }, - { - "inputs": [], - "name": "ReinitializableBase_ZeroInitVersion", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 37f0b1238a1..145f0d291a3 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -172,8 +172,8 @@ "sourceCodeHash": "0x9baa0f9e744cc0ecc61d0fade8bffc18321b228833ea0904dc645f3975be9ed1" }, "src/governance/ProposalValidator.sol:ProposalValidator": { - "initCodeHash": "0xd97f520aa5e4b9b7536bc8fc6d655168d015c3b07161179ef399e7ca358a8e4f", - "sourceCodeHash": "0x78501a104a776d6b2b28e676c7907c0a7169962530df4d6915c9e618e63c5881" + "initCodeHash": "0x3d2dc05bda4ddd8412b16da644ef8d2cbffb0b93c9ba326639ae73c7eedd890d", + "sourceCodeHash": "0x23e62fcea15b38d3e121d8d880ed097b01eefcd7343607bf74ae9f20b620b8b6" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json index 167b206ea9c..683eee9d9b1 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/ProposalValidator.json @@ -1,65 +1,37 @@ [ - { - "bytes": "1", - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "uint8" - }, - { - "bytes": "1", - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "bool" - }, - { - "bytes": "1600", - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "uint256[50]" - }, { "bytes": "20", "label": "_owner", "offset": 0, - "slot": "51", + "slot": "0", "type": "address" }, - { - "bytes": "1568", - "label": "__gap", - "offset": 0, - "slot": "52", - "type": "uint256[49]" - }, { "bytes": "32", "label": "proposalDistributionThreshold", "offset": 0, - "slot": "101", + "slot": "1", "type": "uint256" }, { "bytes": "32", "label": "votingCycles", "offset": 0, - "slot": "102", + "slot": "2", "type": "mapping(uint256 => struct ProposalValidator.VotingCycleData)" }, { "bytes": "32", "label": "proposalTypesData", "offset": 0, - "slot": "103", + "slot": "3", "type": "mapping(enum ProposalValidator.ProposalType => struct ProposalValidator.ProposalTypeData)" }, { "bytes": "32", "label": "_proposals", "offset": 0, - "slot": "104", + "slot": "4", "type": "mapping(uint256 => struct ProposalValidator.ProposalData)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/governance/ProposalValidator.sol b/packages/contracts-bedrock/src/governance/ProposalValidator.sol index 289dec0e0b3..30adec8219a 100644 --- a/packages/contracts-bedrock/src/governance/ProposalValidator.sol +++ b/packages/contracts-bedrock/src/governance/ProposalValidator.sol @@ -2,26 +2,24 @@ pragma solidity 0.8.15; // Contracts -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; import { IProposalTypesConfigurator } from "interfaces/governance/IProposalTypesConfigurator.sol"; import { IEAS, Attestation } from "src/vendor/eas/IEAS.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISemver } from "interfaces/universal/ISemver.sol"; import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; -/// @custom:proxied true /// @title ProposalValidator /// @notice The ProposalValidator contract is responsible for validating proposals and moving /// them to the vote phase on the Optimism Governor. -contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { +contract ProposalValidator is Ownable, ISemver { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -253,6 +251,7 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { mapping(uint256 => ProposalData) internal _proposals; /// @notice Constructs the ProposalValidator contract. + /// @param _owner The address that will own the contract, should be the OP Foundation. /// @param _governor The Optimism Governor contract address. /// @param _approvedProposerAttestationSchemaUid The schema UID for attestations in the Ethereum Attestation Service /// for checking if the caller @@ -261,44 +260,15 @@ contract ProposalValidator is OwnableUpgradeable, ReinitializableBase, ISemver { /// checking if the caller /// is part of the top100 delegates. constructor( + address _owner, IOptimismGovernor _governor, bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid - ) - ReinitializableBase(1) - { + ) { + _transferOwnership(_owner); GOVERNOR = _governor; APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID = _approvedProposerAttestationSchemaUid; TOP_DELEGATES_ATTESTATION_SCHEMA_UID = _topDelegatesAttestationSchemaUid; - _disableInitializers(); - } - - /// @notice Initializes the ProposalValidator contract. - /// @param _owner The address that will own the contract. - /// @param _proposalDistributionThreshold The max amount of tokens that can be distributed in a proposal. - /// @param _proposalTypes Array of proposal types to set data for. - /// @param _proposalTypesData Array of proposal type data corresponding to the proposal types. - function initialize( - address _owner, - uint256 _proposalDistributionThreshold, - ProposalType[] memory _proposalTypes, - ProposalTypeData[] memory _proposalTypesData - ) - external - reinitializer(initVersion()) - { - if (_proposalTypes.length != _proposalTypesData.length) { - revert ProposalValidator_ProposalTypesDataLengthMismatch(); - } - - _setProposalDistributionThreshold(_proposalDistributionThreshold); - - for (uint256 i = 0; i < _proposalTypes.length; i++) { - _setProposalTypeData(_proposalTypes[i], _proposalTypesData[i]); - } - - __Ownable_init(); - transferOwnership(_owner); } /// @notice Submits a Protocol/Governor Upgrade or Maintenance Upgrade proposal. diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 83e0da1a2a6..7ed41bdb247 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -13,7 +13,6 @@ import { RevocationRequestData } from "src/vendor/eas/IEAS.sol"; import { ISchemaRegistry, ISchemaResolver } from "src/vendor/eas/ISchemaRegistry.sol"; -import { IProxy } from "interfaces/universal/IProxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IApprovalVotingModule } from "interfaces/governance/IApprovalVotingModule.sol"; import { IOptimisticModule } from "interfaces/governance/IOptimisticModule.sol"; @@ -23,7 +22,6 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; -import { Proxy } from "src/universal/Proxy.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -36,11 +34,12 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// @notice A test contract that exposes the private _hashProposalWithModule function contract ProposalValidatorForTest is ProposalValidator { constructor( + address _owner, IOptimismGovernor _governor, bytes32 _approvedProposerAttestationSchemaUid, bytes32 _topDelegatesAttestationSchemaUid ) - ProposalValidator(_governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) + ProposalValidator(_owner, _governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) { } function hashProposalWithModule( @@ -134,7 +133,6 @@ contract ProposalValidator_TestInit is CommonTest { address optimisticVotingModule; ProposalValidatorForTest public validator; - ProposalValidatorForTest public impl; IOptimismGovernor public governor; IProposalTypesConfigurator public proposalTypesConfigurator; @@ -455,7 +453,9 @@ contract ProposalValidator_TestInit is CommonTest { approvalVotingModule, votingModuleData_, keccak256(bytes(_proposalDescription)) ); - validator.setProposalData(proposalId_, _proposer, _proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER); + validator.setProposalData( + proposalId_, _proposer, _proposalType, false, PROPOSAL_REQUIRED_APPROVALS, CYCLE_NUMBER + ); } /// @notice Helper function to setup proposal types configurator mocks @@ -528,18 +528,16 @@ contract ProposalValidator_TestInit is CommonTest { // Create mock addresses proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - impl = new ProposalValidatorForTest( - governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID + validator = new ProposalValidatorForTest( + owner, governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID ); - validator = ProposalValidatorForTest(address(new Proxy(owner))); vm.startPrank(owner); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) - ); - // set the data for one voting cycle - validator.setVotingCycleData(CYCLE_NUMBER, START_TIMESTAMP, DURATION, VOTING_CYCLE_DISTRIBUTION_LIMIT); + validator.setProposalDistributionThreshold(PROPOSAL_DISTRIBUTION_THRESHOLD); + for (uint256 i = 0; i < proposalTypes.length; i++) { + validator.setProposalTypeData(proposalTypes[i], proposalTypesData[i]); + } + validator.setVotingCycleData(CYCLE_NUMBER, START_TIMESTAMP, DURATION, PROPOSAL_DISTRIBUTION_THRESHOLD); vm.stopPrank(); } @@ -612,90 +610,6 @@ contract ProposalValidator_TestInit is CommonTest { } } -/// @title ProposalValidator_Version_Test -/// @notice Tests for the version function -contract ProposalValidator_Version_Test is ProposalValidator_TestInit { - function test_version_succeeds() public view { - string memory versionString = validator.version(); - assertEq(versionString, "1.0.0"); - } -} - -/// @title ProposalValidator_Initialize_Test -/// @notice Tests for the initialize function -contract ProposalValidator_Initialize_Test is ProposalValidator_TestInit { - /// @dev Override to create validator proxy without initialization for testing - function _initializeValidator() internal override { - // Create mock addresses - proposalTypesConfigurator = IProposalTypesConfigurator(makeAddr("proposalTypesConfigurator")); - - impl = new ProposalValidatorForTest( - governor, APPROVED_PROPOSER_ATTESTATION_SCHEMA_UID, TOP_DELEGATES_ATTESTATION_SCHEMA_UID - ); - validator = ProposalValidatorForTest(address(new Proxy(owner))); - } - - function test_initialize_succeeds() public { - ( - ProposalValidator.ProposalType[] memory proposalTypes, - ProposalValidator.ProposalTypeData[] memory proposalTypesData - ) = _getProposalTypesAndData(); - - vm.prank(owner); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) - ); - - // Verify initialization was successful - assertEq(validator.proposalDistributionThreshold(), PROPOSAL_DISTRIBUTION_THRESHOLD); - assertEq(validator.owner(), owner); - - // Verify proposal type data - for (uint256 i = 0; i < proposalTypes.length; i++) { - (uint256 requiredApprovals, uint8 idInConfigurator) = validator.proposalTypesData(proposalTypes[i]); - if (proposalTypes[i] == ProposalValidator.ProposalType.MaintenanceUpgrade) { - assertEq(requiredApprovals, 0); - } else { - assertEq(requiredApprovals, PROPOSAL_REQUIRED_APPROVALS); - } - - // GovernanceFund, CouncilBudget, and CouncilMemberElections use APPROVAL_VOTING_MODULE_ID - if ( - proposalTypes[i] == ProposalValidator.ProposalType.GovernanceFund - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilBudget - || proposalTypes[i] == ProposalValidator.ProposalType.CouncilMemberElections - ) { - assertEq(idInConfigurator, APPROVAL_VOTING_MODULE_ID); - } else { - // ProtocolOrGovernorUpgrade and MaintenanceUpgrade use OPTIMISTIC_VOTING_MODULE_ID - assertEq(idInConfigurator, OPTIMISTIC_VOTING_MODULE_ID); - } - } - } - - function test_initialize_mismatchedArrayLengths_reverts() public { - ProposalValidator.ProposalType[] memory proposalTypes = new ProposalValidator.ProposalType[](3); - proposalTypes[0] = ProposalValidator.ProposalType.ProtocolOrGovernorUpgrade; - proposalTypes[1] = ProposalValidator.ProposalType.MaintenanceUpgrade; - proposalTypes[2] = ProposalValidator.ProposalType.CouncilMemberElections; - - // Create mismatched array with different length - ProposalValidator.ProposalTypeData[] memory proposalTypesData = new ProposalValidator.ProposalTypeData[](2); - proposalTypesData[0] = - ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 0 }); - proposalTypesData[1] = - ProposalValidator.ProposalTypeData({ requiredApprovals: PROPOSAL_REQUIRED_APPROVALS, idInConfigurator: 1 }); - - vm.prank(owner); - vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); - IProxy(payable(address(validator))).upgradeToAndCall( - address(impl), - abi.encodeCall(impl.initialize, (owner, PROPOSAL_DISTRIBUTION_THRESHOLD, proposalTypes, proposalTypesData)) - ); - } -} - /// @title ProposalValidator_SubmitUpgradeProposal_Test /// @notice Happy path tests for submitUpgradeProposal function contract ProposalValidator_SubmitUpgradeProposal_Test is ProposalValidator_TestInit { From 1300c98ac22d973484690af930e6e72ed42e76e4 Mon Sep 17 00:00:00 2001 From: IamFlux <175354924+0xiamflux@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:39:21 -0400 Subject: [PATCH 73/73] fix: proposal validator pre-pr (#483) * test: move helper contract out of test * test: rename test contract * chore: semver lock * test: update natspec for tests --- .../snapshots/semver-lock.json | 2 +- .../test/governance/ProposalValidator.t.sol | 79 ++----------------- .../test/mocks/ProposalValidatorForTest.sol | 77 ++++++++++++++++++ 3 files changed, 85 insertions(+), 73 deletions(-) create mode 100644 packages/contracts-bedrock/test/mocks/ProposalValidatorForTest.sol diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 145f0d291a3..a75dbb27efd 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -173,7 +173,7 @@ }, "src/governance/ProposalValidator.sol:ProposalValidator": { "initCodeHash": "0x3d2dc05bda4ddd8412b16da644ef8d2cbffb0b93c9ba326639ae73c7eedd890d", - "sourceCodeHash": "0x23e62fcea15b38d3e121d8d880ed097b01eefcd7343607bf74ae9f20b620b8b6" + "sourceCodeHash": "0x6f62e5285eb2d328f053d168dd7312d16b1557f9e9a08025c18f1916367c4b26" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", diff --git a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol index 7ed41bdb247..c39acdd6712 100644 --- a/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol +++ b/packages/contracts-bedrock/test/governance/ProposalValidator.t.sol @@ -23,6 +23,9 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; // Contracts import { ProposalValidator } from "src/governance/ProposalValidator.sol"; +// Mocks +import { ProposalValidatorForTest } from "test/mocks/ProposalValidatorForTest.sol"; + // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -30,75 +33,6 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -/// @title ProposalValidatorForTest -/// @notice A test contract that exposes the private _hashProposalWithModule function -contract ProposalValidatorForTest is ProposalValidator { - constructor( - address _owner, - IOptimismGovernor _governor, - bytes32 _approvedProposerAttestationSchemaUid, - bytes32 _topDelegatesAttestationSchemaUid - ) - ProposalValidator(_owner, _governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) - { } - - function hashProposalWithModule( - address _module, - bytes memory _proposalData, - bytes32 _descriptionHash - ) - public - view - returns (uint256) - { - return _hashProposalWithModule(_module, _proposalData, _descriptionHash); - } - - /// @notice Exposes proposal data for testing - function getProposalData(uint256 _proposalId) - public - view - returns ( - address proposer_, - ProposalType proposalType_, - bool movedToVote_, - uint256 approvalCount_, - uint256 votingCycle_ - ) - { - ProposalData storage proposal = _proposals[_proposalId]; - return ( - proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle - ); - } - - function setProposalData( - uint256 _proposalId, - address _proposer, - ProposalType _proposalType, - bool _movedToVote, - uint256 _approvalCount, - uint256 _votingCycle - ) - public - { - _proposals[_proposalId].proposer = _proposer; - _proposals[_proposalId].proposalType = _proposalType; - _proposals[_proposalId].movedToVote = _movedToVote; - _proposals[_proposalId].approvalCount = _approvalCount; - _proposals[_proposalId].votingCycle = _votingCycle; - } - - function mockApproveProposal(uint256 _proposalId, address _delegate) public { - _proposals[_proposalId].delegateApprovals[_delegate] = true; - } - - /// @notice Check if a delegate has approved a proposal - function hasDelegateApproved(uint256 _proposalId, address _delegate) public view returns (bool hasApproved_) { - return _proposals[_proposalId].delegateApprovals[_delegate]; - } -} - /// @title ProposalValidator_TestInit /// @notice Setup contract for ProposalValidator tests contract ProposalValidator_TestInit is CommonTest { @@ -2979,9 +2913,10 @@ contract ProposalValidator_SetProposalTypeData_Test is ProposalValidator_TestIni } } -/// @title ProposalValidator_HashProposalWithModule_Test -/// @notice Tests for the hashProposalWithModule function -contract ProposalValidator_HashProposalWithModule_Test is ProposalValidator_TestInit { +/// @title ProposalValidator_Uncategorized_Test +/// @notice Tests for the `_hashProposalWithModule` function that is not part of the public interface +/// @dev This internal function is only exposed through the ProposalValidatorForTest contract +contract ProposalValidator_Uncategorized_Test is ProposalValidator_TestInit { function testFuzz_hashProposalWithModule_succeeds( address fuzzedModule, bytes memory fuzzedProposalData, diff --git a/packages/contracts-bedrock/test/mocks/ProposalValidatorForTest.sol b/packages/contracts-bedrock/test/mocks/ProposalValidatorForTest.sol new file mode 100644 index 00000000000..16d4f49eb6a --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/ProposalValidatorForTest.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Interfaces +import { IOptimismGovernor } from "interfaces/governance/IOptimismGovernor.sol"; + +// Contracts +import { ProposalValidator } from "src/governance/ProposalValidator.sol"; + +/// @title ProposalValidatorForTest +/// @notice A test contract that exposes the private _hashProposalWithModule function +contract ProposalValidatorForTest is ProposalValidator { + constructor( + address _owner, + IOptimismGovernor _governor, + bytes32 _approvedProposerAttestationSchemaUid, + bytes32 _topDelegatesAttestationSchemaUid + ) + ProposalValidator(_owner, _governor, _approvedProposerAttestationSchemaUid, _topDelegatesAttestationSchemaUid) + { } + + function hashProposalWithModule( + address _module, + bytes memory _proposalData, + bytes32 _descriptionHash + ) + public + view + returns (uint256) + { + return _hashProposalWithModule(_module, _proposalData, _descriptionHash); + } + + /// @notice Exposes proposal data for testing + function getProposalData(uint256 _proposalId) + public + view + returns ( + address proposer_, + ProposalType proposalType_, + bool movedToVote_, + uint256 approvalCount_, + uint256 votingCycle_ + ) + { + ProposalData storage proposal = _proposals[_proposalId]; + return ( + proposal.proposer, proposal.proposalType, proposal.movedToVote, proposal.approvalCount, proposal.votingCycle + ); + } + + function setProposalData( + uint256 _proposalId, + address _proposer, + ProposalType _proposalType, + bool _movedToVote, + uint256 _approvalCount, + uint256 _votingCycle + ) + public + { + _proposals[_proposalId].proposer = _proposer; + _proposals[_proposalId].proposalType = _proposalType; + _proposals[_proposalId].movedToVote = _movedToVote; + _proposals[_proposalId].approvalCount = _approvalCount; + _proposals[_proposalId].votingCycle = _votingCycle; + } + + function mockApproveProposal(uint256 _proposalId, address _delegate) public { + _proposals[_proposalId].delegateApprovals[_delegate] = true; + } + + /// @notice Check if a delegate has approved a proposal + function hasDelegateApproved(uint256 _proposalId, address _delegate) public view returns (bool hasApproved_) { + return _proposals[_proposalId].delegateApprovals[_delegate]; + } +}