Skip to content

Conversation

@0xOneTony
Copy link

Description

Following the design doc ethereum-optimism/design-docs#260, this PR details the specs for the new ProposalValidator contract

@0xOneTony 0xOneTony requested review from a team as code owners May 12, 2025 17:09
@0xOneTony 0xOneTony marked this pull request as draft May 12, 2025 17:09
@0xOneTony 0xOneTony changed the title feat: governance validator specs feat: governance proposal validator specs May 12, 2025
@0xOneTony 0xOneTony marked this pull request as ready for review May 12, 2025 17:19
Copy link
Contributor

@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 reviewed this up to about the halfway mark which covers most of the business logic. Will return to the rest, but this feels like enough meat to start making improvements.

Main open item I need to figure out is how we populate the election calldata. Need to investigate how Agora does this and will return here


Submits a `GovernanceFund` or `CouncilBudget` proposal type that transfers OP tokens for approval and voting.

- MUST only be called for `GovernanceFund` or `CouncilBudget` proposal type
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be explicit -- these two types should only split out/separate to the extent that it's useful for frontends to differentiate between them. Other than that, all onchain logic should be identical between these two types. I don't see anything that suggests otherwise yet but felt worth a call-out.

Copy link
Author

Choose a reason for hiding this comment

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

Definitely, since these 2 types require the same checks there wont be any difference when it comes to smart contract logic

- MUST provide a valid attestation UID
- MUST NOT transfer any tokens or change any allowances
- MUST emit `ProposalSubmitted` event
- MUST store proposal data
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's try to specify what exactly we mean by "proposal data" in this context. I don't think it requires all arguments to submitProposal to be stored. Once I review the actual functions, I will try to return to this comment and specify what I think it would be.

Copy link
Author

Choose a reason for hiding this comment

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

I agree, we will make this more descriptive on what the "proposal data" are. In the meantime I will give some context here. We use this mapping to store each proposal data

`_proposals`
A mapping that stores each submitted proposals’ data based on its `proposalHash`. The proposal hash is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array and the `description`
```solidity
mapping(bytes32 => ProposalSubmissionData) private _proposals;
```

where ProposalSubmissionData are defined here

`ProposalSubmissionData`
A struct that holds all the data for a single proposal. Consists of:
- `proposer`: The address that submitted the proposal
- `proposalType`: The type of the proposal
- `proposalTypeConfigurator`: The voting type for the proposal
- `inVoting`: Returns true if the proposal has already been submitted for voting
- `delegateApprovals`: Mapping of addresses that approved the specific proposal
- `approvalsCounter`: The number of approvals the specific proposal has received
```solidity
struct ProposalSubmissionData {
address proposer;
ProposalType proposalType;
uint8 proposalTypeConfigurator;
bool inVoting;
mapping(address => bool) delegateApprovals;
uint256 approvalsCounter;
}
```

Do you think we could remove any of the structs properties?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks yes, I made this comment before reading further down, it makes more sense with the struct specified.

Do you think we could remove any of the structs properties?

proposalTypeConfigurator, pending other discussions on this topic :)

- MUST only be called for `ProtocolOrGovernorUpgrade` , `MaintenanceUpgradeProposals`, or `CouncilMemberElections` types
- MUST be called by an approved address
- MUST check if the proposal is a duplicate
- MUST provide a valid proposal type configurator
Copy link
Contributor

Choose a reason for hiding this comment

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

This should not be necessary I believe. There is a 1:1 mapping of ProposalType as defined in this spec to the ProposalTypesConfigurator uint in the existing governor. This is defined in column G here

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for pointing this out, we initially thought to not have this, and tie the ProposalTypeConfigurtator to each ProposalType like you mentioned, however we were not sure if these are possible to change in the future or have a ProposalType to be able to choose between more than 1 ProposalTypeConfigurator thats why we defined it as is.
If these ProposalTypeConfigurators WILL NOT change any time in the future we could use the mapping and remove the check, this would make things much more simple for us.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is very low probability that we break 1:1 invariant any time soon. Further, it is always possible to add additional ProposalTypes to implement the same functionality and maintain the 1:1 invariant. IMO this might be a better pattern anyway, so my intuition is the simpler thing is better.

Comment on lines 72 to 74
address[] memory _targets,
uint256[] memory _values,
bytes[] memory _calldatas,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
address[] memory _targets,
uint256[] memory _values,
bytes[] memory _calldatas,

In the future this may change, but for MVP, none of these proposal types are ever expected to create a call from the governor. So instead what we want to do is either:

  • enforce that submitProposal stores [0x00..00],[0],[0x] for any proposal submitted via this function
  • enforce that moveToVote only passes those zero values as calldata to the OptimismGovernor.propose function
    Depending on which feels cleaner/easier :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I am realizing that there is an exception to the above for Approval votes (elections) because we need to specify a number of options. The options should still be no-ops but there needs to be some submission step here which I don't fully understand. Effectively, what I'm saying above that we just enforce no-op still applies, but those no-ops will within this struct, which is embedded in an array in the overall proposal struct for this type.. I will follow up and figure out exactly how this logic works, I am having trouble understanding. For reference, here is an example election proposal; I'm struggling to decode the data, need a night's sleep first 😅

bytes[] memory _calldatas,
string memory _description,
ProposalType _proposalType,
uint8 _proposalTypeConfiguration,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
uint8 _proposalTypeConfiguration,

See above comment, _proposalType is sufficient to infer this value

- Proposal MUST have gathered X amount of approvals by top delegates
- Proposal MUST be moved to vote during a valid voting cycle
- MUST check if proposal has already moved for voting
- MUST check if the total amount of tokens that can possible be distributed during this voting cycle does not go over the `VotingCycleData.votingCycleDistributionLimit`
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's discuss on the call tomorrow the implementation complexity of this approach. We are comfortable with a per-proposal-only cap if it simplifies things a lot.

returns (uint256 governorProposalId_)
```

`canSignOff`
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can we call this canApproveProposal, feels a bit more obvious what it is than "sign off"

Copy link
Author

Choose a reason for hiding this comment

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

I agree, will change it!

Comment on lines 146 to 149
address[] memory _targets,
uint256[] memory _values,
bytes[] memory _calldata,
string memory description
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
address[] memory _targets,
uint256[] memory _values,
bytes[] memory _calldata,
string memory description
string memory description,
ProposalType memory _proposalType,
address[] memory candidates, // only for elections
address _recipient, // only for funding proposals
uint256 _amount // only for funding proposals

I think it will be something like this, may want to break out a few of these functions/signature overload or whatever

- MUST emit `ProposalMovedToVote` event

```solidity
function moveToVote(
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't think of a reason that we would need it right now, but this smells like a place where traditionally there would be some sort of nonce stored and incremented during proposal submission and then provided to moveToVote. I can't think of what would go wrong without it though. It would mean you can't submit identical proposals multiple times, but changing the description is also very easy. Let's discuss live.

function setDistributionThreshold(uint256 _threshold) external
```

`setProposalTypeApprovalThreshold`
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see, we made this configurable. I think that's okay, it would be nice if we can default to 4 in the constructor so we don't have to remember to call this unless we change in the future

0xOneTony and others added 5 commits May 19, 2025 17:44
* fix: linting

* fix: remove type configurator as input
* fix: linting

* fix: remove type configurator as input

* fix: submit functions

* fix: define approval timing

* feat: add voting module info

* fix: proposal uniquness

* fix: move to vote

* feat: add new event for voting module data

* fix: proposals mapping description

* fix: namings

* fix: descriptions
* fix: improvements

* feat: improve move to vote

* fix: naming

* fix: remove storage of encoded data
@0xOneTony 0xOneTony requested a review from ben-chain June 2, 2025 12:40
Copy link
Contributor

@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.

Looking good! I think the biggest concern I realized in this review is the delegate approval threshold being dynamic / changing each voting cycle as delegations change. I proposed one way to do it that seems ok, but let's discuss in discord how painful this is to implement in practice


### Public Functions

`submitProtocolOrGovernorUpgradeProposal`
Copy link
Contributor

Choose a reason for hiding this comment

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

fwiw if this is killing you I'm fine with just submitUpgradeProposal or submitRegularUpgradeProposal or something :P

Copy link
Author

Choose a reason for hiding this comment

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

perfect! I prefer the sorter version aswell just wanted to be as verbose but I think submitUpgradeProposal is fine aswell


## Roles

The contract has a single `owner` role (Optimism Foundation) with permissions to:
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I didn't think to ask -- do we read this storage address to check for the EAS attestations as well? That should probs be in this list if so. If not, curious how it's done otherwise

Copy link
Author

Choose a reason for hiding this comment

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

Yes we do read the address to make sure that the attester is the owner address

```solidity
function submitProtocolOrGovernorUpgradeProposal(
uint128 _criteriaValue,
string[] memory _optionDescriptions,
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the options should always be yes/no here for upgrades, so we can omit from this method and hardcode in the function. Following up w/ gov team if there is a third abstain option, but I believe there is not.

Copy link
Author

Choose a reason for hiding this comment

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

beautiful! will adjust to the 2 options

Copy link
Contributor

Choose a reason for hiding this comment

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

Whoops, I was incorrect -- it should still be hardcoded, but options are yes/no/abstain

) external returns (bytes32 proposalHash_);
```

Approval Voting Module
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Approval Voting Module
**Approval Voting Module**

nit


Submits a Protocol or Governor Upgrade proposal for approval and voting.

- MUST be called by an approved address
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- MUST be called by an approved address
- MUST be called by an `owner`-approved address

clarity nit assuming my question on the owner role is yes

Comment on lines 106 to 107
uint248 _againstThreshold,
bool _isRelativeToVotableSupply,
Copy link
Contributor

Choose a reason for hiding this comment

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

We haven't documented these values here, but I believe they can be omitted from this method. I believe that this should always be 12%, true -- meaning, if more than 12% of votable supply chooses veto option, the proposal is rejected. I think hardcoding should be fine

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 Perfect this makes things more simple for the specific proposal, do we know if these numbers could change in the future?

Copy link
Contributor

Choose a reason for hiding this comment

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

Same story as previous comment -- I will follow up tomorrow. It is definitely always true but there might be a few options to %.

This requires the user who submits the proposal to provide some additional data related to the proposal.

For the `ProposalSettings` of the voting module, these are:
- `uint128 criteriaValue`: Since the passing criteria type is always "Threshold" for this proposal type
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe that this should be a fixed % of votable supply. So I think we can remove it as an argument and just pass fixed % of the votable supply oracle value


- This type does not require any checks and is being forwarded to the Governor contracts, this should happen atomically.

For `CouncilMemberElections`, `GovernanceFund` and `CouncilBudget` types:
Copy link
Contributor

Choose a reason for hiding this comment

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

Elections do not do fund transfers. So I believe that the _optionsAmounts/recipients should be checked as zero in the CouncilMemberElections case, same as upgrades case

Copy link
Author

Choose a reason for hiding this comment

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

Correct, for the CouncilMemberElections we will be checking to make sure these fields are empty, will update the document to mention this


`setMinimumVotingPower`

Sets the minimum voting power a delegate must have in order to be eligible to approve a proposal.
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically if the canApproveProposal check is based on the start of the voting cycle, wouldn't this need to be indexed by voting cycle or something?

If the associated complexity is significant, we can think about other options. But if there's a voting cycle storage object, it might just be that you setMinimumVotingPower(_power, _cycleId) or something here instead

struct VotingCycleData {
uint256 startingBlock;
uint256 duration;
uint256 votingCycleDistributionLimit;
Copy link
Contributor

Choose a reason for hiding this comment

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

So I guess we could put the approvalThreshold here to do it per-cycle? I guess the issue would then be the OF needs to set it quickly, or we might have the wrong value used in a canApprove check.

Maybe we can block delegate approvals until the threshold is set. So the flow would be:

  • initial voting cycles have a new approvalThreshold field, all are initialized to 0
  • once a cycle has started and we know what the threshold should be and set to that value
  • approveProposal requires the above step was complete/nonzero value

Does that feel doable? Any other ideas?

Copy link
Author

Choose a reason for hiding this comment

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

Agreed to keep this as is for the MVP

* fix: submit protocol or governor upgrade function name

* fix: remove options from upgrade proposals

* fix: bold for modules info title

* fix: specify proposer approved address must be attested by the owner

* fix: optimistic voting module description

* fix: move to vote cases description

* fix: wording on approval module criteria value
Comment on lines 83 to 85
- `uint128 criteriaValue`: Since the passing criteria type is always "Threshold", for this proposal type,
this value will be the percentage that will be used to calculate the fraction of the votable supply that
the proposal will need in votes in order to pass.
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 If the % is fixed we can even remove this as an input and hardcode it

0xOneTony and others added 5 commits June 10, 2025 17:49
* fix: top100 delegate calculation to use EAS

* fix: function seperation

* fix: function seperation

* fix: headers
Copy link
Contributor

@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.

This is looking great. I'd like us to fill in the non-maintenance upgrade method for completion, but everything else looks spot on, no comments.


`submitUpgradeProposal`

Submits a Protocol/Governor Upgrade or a Maintenance Upgrade proposal to move for voting.
Copy link
Contributor

Choose a reason for hiding this comment

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

This should have a method spec, right?

I think it would be the same exact signature and requirements, minus can move straight to voting if all submission checks pass

Copy link
Author

Choose a reason for hiding this comment

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

The idea is to have the same function for both of these types since most of the logic is the same, however we improved the text and params here! d23b082

* fix: submit upgrade proposal

* fix: text
* fix: spec inconsistencies

* fix: linting
Copy link
Contributor

@mds1 mds1 left a comment

Choose a reason for hiding this comment

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

Approving on behalf of Ben to unblock merge

* fix: can approve proposal visibility

* fix: event name

* fix: proposal data field name

* fix: voting cycle data field name

* fix: submit funding proposal invariant

* fix: criteria value description

* fix: spelling

* fix: approved proposer schema

* fix: against threshold description
- MUST use the `Optimistic` Voting Module
- MUST provide a valid againstThreshold
- MUST provide a valid attestation UID
- MUST NOT execute any operations
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the definition of operations here?

Copy link
Author

Choose a reason for hiding this comment

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

This should be removed from here since these proposals are using the Optimistic voting module. It is also mentioned for the Council Member Elections proposals but it should stay there (will rephrase it to be more clear). Basically the Approval voting module has this ProposalOption struct https://github.com/voteagora/optimism-governor/blob/3f8a2ea29d2c91a176758c72a213d499424ee2fc/src/modules/ApprovalVotingModule.sol#L35-L41
that can include some arbitary calldata, so the meaning of operations is basically that these calldata should be empty and thus not execute any further operations.
An other example of the Approval voting module which for this case should include some calldata is for the funding proposal types where we have to execute a transfer call if the proposal passes.

@0xOneTony 0xOneTony marked this pull request as draft August 21, 2025 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants