Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC-191] On-chain governance #1519

Merged
merged 23 commits into from
Sep 5, 2023
Merged

Conversation

goran-ethernal
Copy link
Collaborator

@goran-ethernal goran-ethernal commented May 17, 2023

Description

This is the PR that introduces on-chain governance to Edge as a hard fork called governance.

By default, when starting a new supernet, governance will be active from the first block.

Tagging @DannyS03 so that he can update the docs based on described changes.

New contracts

There are 4 new contracts that need to be deployed on a supernet:

  1. ChildGovernor - main governance contract that implements all the main functions for proposals, like propose, execute, cancel, etc. It is an implementation of openzeppelin Governor contract, so it is Bravo compatible, and can be used with Tally (a UI for managing governance proposals on a network).
  2. ChildTimelock - a contract that handles who can propose, execute and vote on a proposal.
  3. NetworkParams - a contract that holds all the network configuration parameters of a supernet, that can be governed.
  4. ForkParams - a contract that holds info about proposed hard forks and from which block are they proposed to become active.

Changes in genesis command

This PR introduces new flags in genesis command, and those are:

  • checkpoint-interval - uint64 flag that represents number of blocks after which a new checkpoint is submitted (default is 900 blocks).
  • withdrawal-wait-period - uint64 flag that represents number of epochs after which withdrawal can be done from child chain (default is 1 epoch).
  • vote-delay - string flag that represents number of blocks after proposal is submitted before voting starts (can be represented by a hex or decimal value, or with big int, default is 10 blocks).
  • vote-period - string flag that represents number of blocks that the voting period for a proposal lasts (can be represented by a hex or decimal value, or with big int, default is 10000 blocks).
  • vote-proposal-threshold - string flag that represents number of vote tokens (in wei) required in order for a voter to submit a proposal (can be represented by a hex or decimal value, or with big int, default is 1000 wei).
  • governor-admin - string flag that represents an address of a governance admin (governance admin can add new or remove old proposers of governance proposals, and add new and remove old executors of accepted proposals. Default is zero address, and that will mean no new addresses can be enabled to propose and execute governance proposals).
  • proposal-quorum - uint64 flag that represents percentage of total supply of vote token needed for a governance proposal to be accepted (from 0 to 100%, default is 67%).

Changes in genesis.json

Because of the new flags, mentioned in above text, genesis.json is now expanded to hold new data:

  • in params forks section, there will be a new fork added if the chain is starting a new called governance:
"params": {
        "forks": {
            "governance": {
                "block": 0
            },
  • in polybft section, two new json parts are added:
"polybft": {
    ...
    "checkpointInterval": 900,
    "withdrawalWaitPeriod": 1,
    ...
}
  • in polybft section, a new json child is added called governanceConfig, that holds all the data in regards to on-chain governance setup:
"polybft": {
    ...
    "governanceConfig": {
         "votingDelay": "0xa",
         "votingPeriod": "0x2710",
         "proposalThreshold": "0x3e8",
         "governorAdmin": "0x26F6003E6d502CB8Fb5DC2ec8DE9489f3a215EdC",
         "proposalQuorumPercentage": 67,
         "childGovernorAddr": "0x000000000000000000000000000000000000100C",
         "childTimelockAddr": "0x000000000000000000000000000000000000100D",
         "networkParamsAddr": "0x000000000000000000000000000000000000100e",
         "forkParamsAddr": "0x000000000000000000000000000000000000100f"
    }
    ...
}

governanceConfig in genesis.json

governanceConfig is a new part of polybft configuration in genesis.json file, and holds these values:

  • votingDelay - is the voting delay setup in genesis command, as explained in previous sections.
  • votingPeriod - is the voting period setup in genesis command, as explained in previous sections.
  • proposalThreshold - is the proposal threshold setup in genesis command, as explained in previous sections.
  • governorAdmin - is the address of governance admin setup in genesis command, as explained in previous sections.
  • proposalQuorumPercentage - is the total voting power percentage needed to accept a proposal, setup in genesis command, as explained in previous sections.
  • childGovernorAddr - address of ChildGovernor contract on supernet.
  • childTimelockAddr - address of ChildTimelock contract on supernet.
  • networkParamsAddr - address of NetworkParams contract on supernet.
  • forkParamsAddr - address of ForkParams contract on supernet.

Governance on a new chain

By default, when starting a new supernet, the governance fork is active from genesis, and its governanceConfig will be populated on genesis command. Also, the new contracts will be automatically deployed.

Governance on an already running chain

To enable governance on a supernet that was already running on an older version, these are the steps that you need to follow:

  1. Deploy the new governance contracts on supernet (deploy them, and initialize them). Save their addresses, since you will need them later.
  2. Upgrade ValidatorSet and RewardPool contracts (because they are modified in regards to on-chain governance).
  3. Edit genesis.json to have checkpointInterval and withdrawalWaitPeriod with values that you provided when initializing NetworkParams contract.
  4. Edit genesis.json to have governanceConfig, and provide addresses of contracts from step 1, provide address of governance admin you specified when you deployed ChildTimelock contract, provide configuration values you specified when you deployed NetworkParams contract.
  5. Edit genesis.json forks section to have governance and specify some future block from which governance becomes active.
  6. Upgrade the nodes to use a new version of code which has governance fork.
  7. Start the nodes.

Network configuration parameters supported in governance

These are the client configuration parameters that can be governed in the on-chain governance:

  • uint256 checkpointBlockInterval - in blocks
  • uint256 epochSize - in blocks
  • uint256 epochReward - in wei
  • uint256 sprintSize - in blocks
  • uint256 minValidatorSetSize - in number of validators
  • uint256 maxValidatorSetSize - in number of validators
  • uint256 withdrawalWaitPeriod - in blocks
  • uint256 blockTime - in seconds
  • uint256 blockTimeDrift - in seconds
  • uint256 votingDelay - in blocks
  • uint256 votingPeriod - in blocks
  • uint256 proposalThreshold - in percent

Requirements when deploying governance contracts

When deploying governance contracts you are required to specify some things.

  1. NetworkParams contract - specify initial network configuration parameters when initializing the contract. When deploying a new supernet, this is done automatically on genesis, since those configuration parameters are read from genesis.json and passed to contract initializer. When deploying the contract manually, you need to do this manually, but make sure those values match the ones in genesis.json. You also need to specify the contract owner. In our case, this is the ChildTimelock contract, because it is the ChildTimelock who executes an appropriate function on the NetworkParams when proposal is executed.
  2. ForkParams contract - specify contract owner. In our case, this is the ChildTimelock contract, because it is the ChildTimelock who executes an appropriate function on the ForkParams when proposal is executed.
  3. ChildTimelock contract - specify minDelay value (which represents the delay after which accepted and queued proposal can be executed, and is specified in number of blocks). You also need to specify list of addresses that represent the initial proposers and executors of governance proposals. Please note that you need to add address of ChildGovernor contract to these lists as well. You can do that after you initialize the contract by calling the grantRole(bytes32 role, address account) function on ChildTimelock contract (this can only be done by the governance admin). You also need to specify the address of governance admin (the one who can add and remove proposers and executioners). When deploying a new supernet, all this is done automatically by reading the governanceConfig in genesis.json, and in this case minDelay is by default 1 block, and can not be changed.
  4. ChildGovernor contract - specify token used for voting. This should be the address of ValidatorSet contract on supernet, since he acts as a wrapper token around stake of validators. We do not recommend using any other token, because they need to implement ERC20VotesUpgradable interface from openzeppelin to be compatible. You also need to specify the addresses of NetworkParams and ChildTimelock contracts, and initial proposalQuorumPercentage from genesis.json file. When deploying a new supernet, all this is done automatically by reading the governanceConfig in genesis.json.

Rules of governance on a supernet

By default, when deploying a new supernet with governance fork enabled, governance is configured like this:

  1. Voting token for voting on governance proposals is the stake token representation on supernet which is the ValidatorSet contract. It implements ERC20VotesUpgradable to be compatible with Bravo governance. You can use some other token when you deploy the contracts manually, but we do not recommend this. Voting token provided by Edge will always be the stake token on L2 (ValidatorSet contract), and this can not be changed through genesis when deploying a new supernet. When deploying manually, we do not limit the users to use another token, but we do not recommend it.
  2. Only inital validator set will be able to propose and execute proposals. This is default behavior provided by Edge when deploying a new supernet, and can not be changed through genesis. governorAdmin can later add new addresses and remove old ones. Please note that proposers need to have vote tokens in their balance in order to propose a proposal if the proposal threshold is greater than 0. When deploying contracts manually, we do not limit the users to specify other addresses. Once the supernet is started, the governorAdmin can add new proposers and executioners.
  3. governorAdmin is taken from the genesis specification and passed to the ChildTimelock contract. Only him can add new and remove old proposers and executioners, as well as admins.

Proposal lifecycle for a change of some network configuration parameter

Proposing a new proposal

  1. First requirement is that user address needs to be in the list of proposers on ChildTimelock contract.
  2. Second requirement is that user has enough vote tokens if the proposalThreshold is greater than zero. If it is zero, no vote tokens are required to propose a change on governance.
  3. Proposal is submitted by calling the:
function propose(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description
)

function on ChildGovernor contract.

  • targets - are the addresses of contracts on which change will be executed. In this case address of NetworkParams contract.
  • values - values of transactions. In this case, it should be 0.
  • calldatas - abi encoded call to desired function on NetworkParams contract. For example if we want to change epochSize, this should be abi encoded call to function setNewEpochSize(uint256 newEpochSize) with specified new epoch size.
  • description - string description of given proposal that other users can see when voting. For example, Changing epoch size from 10 to 100 blocks.
    Important note here is to remember, that targets, values and calldatas are paired lists, and need to have the same amount of items.
  1. Once the proposal is submitted, and after voteDelay number of blocks passes, the voting can start.

Voting on a proposal

To vote on a proposal, voters need to have some vote tokens in order to have influence on the proposal acceptance, and the voteDelay number of blocks needs to pass, otherwise, the vote will not be counted. There is no limit who can vote on a proposal, but they can only vote once for a given proposal. Voters can even cast a vote if they don't have any tokens, but they will not have any influence on the proposal.
Voting is done by calling a function from the castVote family of functions on ChildGovernor:

function castVote(uint256 proposalId, uint8 support)
function castVoteWithReason(
    uint256 proposalId,
    uint8 support,
    string calldata reason
)
function castVoteWithReasonAndParams(
    uint256 proposalId,
    uint8 support,
    string calldata reason,
    bytes memory params
)
function castVoteBySig(
    uint256 proposalId,
    uint8 support,
    uint8 v,
    bytes32 r,
    bytes32 s
)
function castVoteWithReasonAndParamsBySig(
    uint256 proposalId,
    uint8 support,
    string calldata reason,
    bytes memory params,
    uint8 v,
    bytes32 r,
    bytes32 s
)

Once the vote is submitted, the voting power (weight, or amount of voting tokens that address had when proposal was submitted) of the vote will be added to required proposal and it's vote type. Users can vote: For, Against or Abstained.
If the proposal has quorum of For votes (meaning, the proposalQuorum percentage of total supply of vote token at the time of proposal submition is reached for For votes), then, that proposal is accepted after the votePeriod ends. Otherwise, it will not be accepted (marked as Defeated on contract).

Queue a proposal

Once the voting period ends, and proposal has quorum of For votes, user needs to queue the porposal for execution. This is done by calling the queue(uint256 proposalId) function on ChildGovernor contract. There is no limit who can queue a proposal.

Execute a proposal

Once the proposal is queued and minDelay of blocks provided to ChildTimelock passes, proposal can be executed. This can be done by any address that is in the executors list on ChildTimelock contract, and by calling:

function execute(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 descriptionHash
)

function on ChildGovernor contract. The user needs to provide the same values as when proposal was submitted, only difference being the keccak256 hash of proposal description string.
Once the proposal is executed, it will be marked on contracts as Executed and no other action can be done on that proposal. It will be kept in contract storage.
When the proposal is executed, the address in targets parameter will be called, by using its calldata. In our example, setNewEpochSize function will be called, which will set the new epoch size on NetworkParams contract, and the new epoch size will be used by the edge client. More about this in later sections.

Canceling a proposal

Canceling a proposal can only be done by the proposer of the proposal, and if proposal is not already executed or canceled. This is done by calling the:

function cancel(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 descriptionHash
)

function on ChildGovernor contract. Notice that the parameters of function are the same as on execute.

Proposal lifecycle for an introduction of some hard fork

The lifecycle of a governance proposal for some hard fork is the same as a proposal of change of some network configuration parameter. Only difference is in setting targets and calldatas fields when proposing and executing proposals.
For example, we want to propose activation of fork testFork from block 100k. We would do that by specifying the address of ForkParams contract in targets parameter, and abi encoded call of addNewFeature(uint256 blockNumber, string calldata feature) function on that contract (where you specified from which block the feature is active and it's name).

When does a change of some network configuration parameter take effect?

Once the proposal for a change of some network configuration parameter is executed, and that parameter gets updated on NetworkParams contract, it's change does not happen immediately. To ensure correctness and liveness of our consensus, we can not just change some network configuration in the middle of an epoch. Changing some critical parameters like block time or epoch size in the middle of an epoch can have serious consequences, because consensus relies on these two fields. Because of that, once that proposal is executed and parameter updated on NetworkParams contract, that event will be caught by the client and saved to it's database. Once the epoch, in which those proposals got executed, finishes, the client will read the new values, and update it's configuration for the next epoch. This ensures correctness of consensus and mitigates possible issues that could arise if these changes are turned on immediately.
There are few exceptions to this. Change to next parameters takes effect immediately once their proposals get executed, since they are not used by the consensus engine and protocol, nor Edge client, but only on contracts:

  • epochReward,
  • withdrawalWaitPeriod,
  • votingDelay,
  • votingPeriod,
  • proposalThreshold.

To have a more clearer picture, check this diagram out:
image

When does a change to activate some hard fork take effect?

Proposals to activate/deactivate some hard fork take effect immediately on the block which was specified as the activation block for that feature. So, for example, if we want to activate london fork on block 100k, and that proposal is executed on block 90k, the edge client will immediately save the info that that fork will be active from block 100k.

How to activate/deactivate a hord fork using on-chain governance?

To activate a fork using on-chain governance users need to:

Send a governance proposal to activate a fork, where proposal calldata must be a call to addNewFeature function on ForkParams contract. In given calldata user must specify fork name and block from which it should be active (fork name in the proposal must match the name of that fork in code:).
Proposal must have enough votes, and executed.
After proposal gets executed, fork will be automatically added to edge client forks where it becomes active from specified block.

If a user didn't specify that fork in genesis, that fork will automatically be turned off when chain starts, and it can be activated by later specifying it in the genesis and setting some future block for it (restart of all nodes is required), or by using on-chain governance.

If a user specified a fork in genesis, and that fork gets activated when its block is reached, there is one way to deactivate it, and it's by using on-chain governance. Basically, user will need to create a proposal that changes it's block number to some block that will either never be reached, or it will be reached after a long period of time (they can always change this using ForkParams contract).

To deactivate a fork using on-chain governance users need to:

Send a governance proposal to deactivate a fork, where proposal calldata must be a call to addNewFeature function on ForkParams contract (if it is not already added to that contract), or updateFeatureBlock (if it was already added to that contract). In given calldata user must specify fork name and some big block number that will not be reached in a long time (fork name in the proposal must match the name of that fork in code:).
Proposal must have enough votes, and executed.
After proposal gets executed, fork will be automatically deactivated on edge client.

Code changes

  1. Deployed and initialized new contracts for governance through genesis.
  2. Updated genesis.json file to hold new configuration.
  3. Updated genesis command with new flags.
  4. Implemented goernanceManager on Edge client which is a handler of governance proposal events. It does these things:
    • on starting of node it will take the genesis client configuration and save it to boltdb if the chain is just started (or the node just started syncing from genesis block). If we are starting from some newer block (in cases when node was down some time), genesis configuration will already be saved to boltdb.
    • on finalization of each block (either from consensus or syncer), iterates through transaction receipts to see if any governance proposal was executed in given block. If it was executed, it will save those events to boltdb under epoch in which proposal was executed.
    • on starting of the new epoch, it will check if there are any found governance events from previous step that happened in the closing epoch. If we had any events we found and saved in boltdb it will iterate through them and update client configuration based on new values of changed network parameters through on chain governance. Updated client configuration will be saved to boltdb. This is done at the end of epoch, because we can not ensure stability if we start changing parameters at the end of epoch. For example, if we are to change epoch size in the middle of epoch, a lot of things like distributing rewards, and commiting an epoch will not work correctly. That is why, we will save executed proposal events to boltdb first, and then at the end of the epoch, update the client configuration based on them.
    • provides a function to get the most current configuration to consensus_runtime, so that the client has the latest parameter values.
  5. consensus_runtime is changed so that now each epoch has it's own client configuration, provided by the governance_manager. A new field is introduced to epochMetadata struct (which holds config and data from current epoch), called CurrentClientConfig, and it will use that field whenever some parameter from client configuration is needed in some part of code.
  6. Distribution of rewards is moved to the first block of epoch because of the change on ValidatorSet contract to implement ERC20VoteUpgradable. It's totalSupplyAt function got changed because of this, and now no longer supports getting totalSupply in the current checkpoint, and this is actively used by RewardPool to have more precise reward distribution. Because of this the distribution of rewards for previous epoch is moved to the first block of next epoch. This has three benefits:
    • It lowers gas consumption on epoch ending blocks.
    • It enables more accurate reward distribution.
    • We can use ValidatorSet as voting token for governance.
  7. To support fork acitvation/deactivation:
    • governanceManager is expanded to collect both NewFeature and UpdatedFeature events that get emitted by ForkParams functions addNewFeature and updateFeatureBlock respectively.
    • Those events will be saved to a separate bucket called forkParamsEventsBucket, where key is hash of fork name and value is block from which that feature is active.
    • On finalization of each block (either from consensus or syncer), governanceManager will get those events from receipts and save them in boltdb. Note, that governanceManager has a means to get missed events (if some error occurred while saving those events in previous block.
    • On each block that had these ForkParams events, it will register and activate/deactivate those forks in forkManager, so that the rest of the code knows if that fork is active or not, and from which block.
  8. Written e2e tests for successful and unsuccessful governance proposals.

Changes include

  • Bugfix (non-breaking change that solves an issue)
  • Hotfix (change that solves an urgent issue, and requires immediate attention)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (change that is not backwards-compatible and/or changes current functionality)

Checklist

  • I have assigned this PR to myself
  • I have added at least 1 reviewer
  • I have added the relevant labels
  • I have updated the official documentation
  • I have added sufficient documentation in code

Testing

  • I have tested this code with the official test suite
  • I have tested this code manually

@goran-ethernal goran-ethernal self-assigned this May 17, 2023
@goran-ethernal goran-ethernal added the feature New update to Polygon Edge label May 17, 2023
@goran-ethernal goran-ethernal force-pushed the feat/on-chain-governance branch 4 times, most recently from 96b15d6 to f7d8f1a Compare July 5, 2023 10:41
@goran-ethernal goran-ethernal force-pushed the feat/on-chain-governance branch 3 times, most recently from 1e53432 to 712d622 Compare July 20, 2023 14:27
@goran-ethernal goran-ethernal marked this pull request as ready for review August 9, 2023 11:20
@goran-ethernal goran-ethernal requested a review from a team August 9, 2023 11:20
@paulgoleary
Copy link
Collaborator

We should strongly consider adding defaultBaseFeeChangeDenom as a governable param. With a low block time, the default value of this param - which is inherited from Geth - allows the 1559 base fee to vary wildly.
We tweaked it 2X (to 16) in Bor but likely should have gone 2X further.

@Stefan-Ethernal
Copy link
Collaborator

We should strongly consider adding defaultBaseFeeChangeDenom as a governable param. With a low block time, the default value of this param - which is inherited from Geth - allows the 1559 base fee to vary wildly. We tweaked it 2X (to 16) in Bor but likely should have gone 2X further.

Added to the backlog and we are going to revisit it in the future.

command/genesis/genesis.go Outdated Show resolved Hide resolved
command/genesis/polybft_params.go Show resolved Hide resolved
command/genesis/polybft_params.go Outdated Show resolved Hide resolved
consensus/polybft/consensus_runtime.go Show resolved Hide resolved
consensus/polybft/consensus_runtime.go Show resolved Hide resolved
consensus/polybft/governance_manager.go Show resolved Hide resolved
consensus/polybft/governance_manager.go Outdated Show resolved Hide resolved
@Nemanja0x Nemanja0x self-requested a review September 5, 2023 05:07
Copy link
Contributor

@vcastellm vcastellm left a comment

Choose a reason for hiding this comment

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

LGTM aside from minor comments

goran-ethernal and others added 23 commits September 5, 2023 17:55
* Add governorAdmin flag

* Comments fix
* Add proposal-quorum flag

* Comments fix
…1735)

* Move distribute rewards to first block of epoch

* Modification of reward distribution as part of governance fork

* Small change

* Comments fix
* Add contracts

* Fix integration test
…proposals (#1749)

* GovernanceManager

* PostEpoch

* PostBlock and update of client config

* Lint fix

* UTs

* Comments fix

* Retry in PostEpoch as well

* Comments fix
* Governance e2e tests

* Comments fix
* Add forks activation based on ForkParams contract

* Comments fix
@goran-ethernal goran-ethernal merged commit 3cae93a into develop Sep 5, 2023
5 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Sep 5, 2023
@goran-ethernal goran-ethernal deleted the feat/on-chain-governance branch September 5, 2023 15:56
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature New update to Polygon Edge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants