Skip to content

Conversation

@0xOneTony
Copy link

Description

This PR introduces the ProposalValidator contract designed to enable permissionless proposals in the Optimism
governance system, as detailed in the design doc and technical spec.

0xChin and others added 30 commits May 6, 2025 13:05
* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* chore: fix immutable variables coding style

* chore: explain governor external call

* docs: explicit Validator/Governor contract interaction in events natspec

---------

Signed-off-by: Chiin <[email protected]>
Co-authored-by: 0xOneTony <[email protected]>
* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* 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 <[email protected]>
Co-authored-by: 0xOneTony <[email protected]>
* 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 <[email protected]>
* chore: remove votingCycleBlock variable

* chore: remove ImmutableProposalTypeData
* 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 <[email protected]>
* 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
* refactor: rename proposalTypeConfigurator to proposalVotingModule

* chore: run pre-pr

* fix: missing rename in comment

* chore: run pre-pr

* docs: fix natspec description
* feat: add hashProposalWithModule function

* chore: run pre-pr

* test: add hashProposalWithModule tests

* refactor: remove hashProposal function

* chore: run pre-pr
* chore: remove submitProposal function

* test: remove usage of submitProposal function
* 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
* 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
* 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
* 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
* 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 <[email protected]>
Signed-off-by: Chiin <[email protected]>

* 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 <[email protected]>
Co-authored-by: 0xOneTony <[email protected]>
* 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
* 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
* 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
* fix: voting window to use timestamp

* fix: pre-pr

* fix: improve test
* 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
* fix: add proposal id validation on submit upgrade proposal returned proposalId

* fix: pre-pr
* refactor: fetch configurator from governor

* fix: pre-pr

* fix: pre-pr
* fix: check for uninitialized voting modules

* fix: pre-pr

* fix: pre-pr
* fix: voting cycle validity on submit

* fix: edge case on upgrade proposals
* fix: budget cap dos

* fix: invalid proposal case

* fix: test

* fix: tests
* 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 <[email protected]>
* refactor: remove canApproveProposal and normalize validate functions

* fix(test): handle invalid voting cycle edge case

* fix: pre-pr
0xChin and others added 16 commits July 29, 2025 19:39
* fix: add proposal id validation on submit upgrade proposal returned proposalId

* fix: pre-pr
* refactor: fetch configurator from governor

* fix: pre-pr

* fix: pre-pr
* fix: check for uninitialized voting modules

* fix: pre-pr

* fix: pre-pr
* fix: voting cycle validity on submit

* fix: edge case on upgrade proposals
* fix: budget cap dos

* fix: invalid proposal case

* fix: test

* fix: tests
* 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 <[email protected]>
* refactor: remove canApproveProposal and normalize validate functions

* fix(test): handle invalid voting cycle edge case

* fix: pre-pr
* 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
* 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
Copy link
Collaborator

@ben-chain ben-chain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to go AFK, so submitting this WIP review for now. This is looking good, I will have some follow up comments for the gov team and there are some discussion points for us to touch on in my comments :D


event Initialized(uint8 version);

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. I initially misinterpreted this as being about proxy-related ownership, which I think it is not. But: it does make me wonder how much complexity we introduce with these sorts of stateful config updates via the owner.

I hadn't thought about it initially, but -- if the OF can simply setAuthorizedProposer, then in theory, this entire contract could be immutable/non-upgradable/non-configurable. setAuthorizedProposer is basically a superset of the upgrade functionality here.

There are pros and cons to this, maybe it is worth a bigger discussion in chat. There are obviously some cons around in-flight proposals, indexers, etc. But actually with indexers for the Governor, we've seen breaking changes cause issues with historic votes. Separating addresses could have benefits there.


function proposalTypesData(ProposalType) external view returns (uint256 requiredApprovals, uint8 idInConfigurator);

function votingCycles(uint256) external view returns (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we return the struct here or is that an anti-pattern?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is a good practice to return the fields instead of the struct for the getter function.

/// 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
/// @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.

Comment on lines 277 to 278
/// @param _duration The duration of the voting cycle.
/// @param _votingCycleDistributionLimit The max amount of tokens that can be distributed during the voting cycle.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading right, this is for the first voting cycle specifically, is that correct?

Unless I'm missing a security property or something, I'd recommend to remove from initialization and just have owner use the setters. We will always set voting cycles in batches/groups/seasons at a time, so no reason to have a special case. But LMK if you had something else in mind with these args

IProposalTypesConfigurator.ProposalType memory proposalTypeConfig =
IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator);

// Validate voting module exists
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I am comfortable with removing this check since ProposalValidator_InvalidUpgradeProposalType is thrown above if it's not a value we know to be true.

If we want to keep it -- feels like checking codesize(proposalTypeConfig.module) != 0 would be more robust than just the name length.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible safer option is that the proposal type id for the approval module is known as well as the address. Is making this constant the best for now? Or adding some role for adding new proposal types / modules if things are redeployed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ben-chain the idea of this check is to mimic the Governor to make sure that a valid submitted proposal will not revert in case it moves to the Governor. It also acts as a safety in case someone changes the voting module data on the ProposalTypeConfigurator
https://github.com/voteagora/optimism-governor/blob/3f8a2ea29d2c91a176758c72a213d499424ee2fc/src/OptimismGovernor.sol#L438

@corydickson The problem is we cant support a completely new module without redeploying cause each proposal has their unique set of rules that the contract will need to adapt for the specific proposal type that use the new module. As long as the voting module's logic doesn't change, then we can support a "different variation" of an existing module by simply updating the id of the configurator for the "new" module with setProposalTypeData.


// Generate unique proposal ID
proposalId_ =
_hashProposalWithModule(votingModule, proposalVotingModuleData, keccak256(bytes(_proposalDescription)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add the _againstThreshold into the randomness here? I can see having to re-submit due to incorrect threshold and running into a collision

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, we can't easily because this is matching the existing Governor Implementation, which assumed no against threshold change like that...

Are there any other security risks associated with the potential for collision here? If not, I guess we can just ignore this issue, and simply add whitespace to the description if we need to re-submit lol.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going back on myself again -- I just noticed that we are already forced to introduce idInConfigurator in other codepaths, so I think we should do my original suggestion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intentionally aim to generate the exact same proposal id as the one that will be created in the Governor, this helps us verify that the proposals are the same in both contracts and that the same proposal has not been already proposed to the Governor. If we want to avoid collisions the easiest way that also works currently would be to re-submit with a slight change in the description like you mentioned

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just adding the proposer to the description would be a good way handle this.

emit ProposalSubmitted(proposalId_, _msgSender(), _proposalDescription, _proposalType);
emit ProposalVotingModuleData(proposalId_, proposalVotingModuleData);

// MaintenanceUpgrade proposals move directly to voting (atomic operation)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this worth making into an internal function? Seems to be shared with the other moveToVote functions


// Validate options length bounds
uint256 optionsLength = _optionDescriptions.length;
if (optionsLength == 0 || optionsLength > type(uint8).max) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check in with the gov team -- there actually may be a desire for an artificially lower max.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be great!

IProposalTypesConfigurator(GOVERNOR.PROPOSAL_TYPES_CONFIGURATOR()).proposalTypes(idInConfigurator);

// Validate voting module exists
if (bytes(proposalTypeConfig.name).length == 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above -- I'm fine with deleting, if we keep, should probably check module codesize instead


// Validate input arrays have matching lengths
uint256 optionsLength = _optionsDescriptions.length;
if (optionsLength != _optionsRecipients.length || optionsLength != _optionsAmounts.length) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a small nit but instead of imposing this check in submitting the proposal should this be moved to _buildApprovalModuleOptions just because this is an always required to check in the approval module and then if we add different proposal types it reduces a duplicated code pattern. Same thing for the optionsLength.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm I am not sure about this check because this is not needed for example on the Council Member elections even though it uses the Approval module, but I believe we should add the check bellow now that you mentioned it if (optionsLength == 0 || optionsLength > type(uint8).max)

0xOneTony and others added 5 commits August 8, 2025 12:26
* fix: add approval timing check

* fix: add missing test
* fix: improve test vars names

* fix: test helpers input names
* fix: make validator immutable

* refactor: add version to ProposalValidator

---------

Co-authored-by: Flux <[email protected]>
* test: move helper contract out of test

* test: rename test contract

* chore: semver lock

* test: update natspec for tests

/// @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 }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do somewhat worry here that we are overloading proposalType as the PTC currently linked to the OP governor has it's own set of proposalTypes that do not use modules etc but don't necessarily map 1-to-1 on the enum:

 enum ProposalType {
        ProtocolOrGovernorUpgrade,
        MaintenanceUpgrade,
        CouncilMemberElections,
        GovernanceFund,
        CouncilBudget
    }

I think if we ensure that these two things map onto each other there will be significantly less confusion. Apologies if this has been addressed already in the spec somewhere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This set of ProposalTypes is linked to the ones defined in the ProposalTypeConfigurator only by the ProposalTypeData.idInConfigurator, they are *not * a 1:1 match as some of the ProposalTypesin the validator share the same id in the configurator, this is because while the validator’s types deal with individual types the ones in the configurator are more focused on the voting process (quorum, approval threshold and the module that should handle it).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, I was raising this to discuss the potential of coalescing them.


// MaintenanceUpgrade proposals move directly to voting (atomic operation)
if (_proposalType == ProposalType.MaintenanceUpgrade) {
proposal.movedToVote = true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be updated after _proposeToGovernor in case the transaction to create the proposal on the governor reverts?

@opgitgovernance opgitgovernance added the S-stale Status: Will be closed unless there is activity label Aug 30, 2025
@opgitgovernance
Copy link
Contributor

This pr has been automatically marked as stale and will be closed in 5 days if no updates

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-stale Status: Will be closed unless there is activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants