Proposal Functions
The Proposal.sol file contains a set of functions that all governance models inherit. Currently, there are four governance models inheriting from Proposal.sol: Bravo, Multisig, Timelock, and OpenZeppelin Governor. When using FPS for any of the aforementioned models to create proposals, all that needs to be done is to inherit one of the proposal types, such as GovernorBravoProposal.sol, and override the necessary functions to create the proposal, like build
and deploy
.
FPS is flexible enough so that for any different governance model, governance proposal types can be easily adjusted to fit into the governance architecture. An example has been provided using Arbitrum Governance on the FPS example repo. The following is a list of functions that the proposals can implement:
name()
: This function is empty in the Proposal contract. Override this function in the proposal-specific contract to define the proposal name. For example:description()
: This function is empty in the Proposal contract. Override this function in the proposal-specific contract to define the proposal description. For example:deploy()
: This function is empty in the Proposal contract. Override this function when there are deployments to be made in the proposal. Here is an example from a Governor Bravo Proposal demonstrating how to deploy two contracts, Vault and Token, if they are not already deployed.Deployments are done by
DEPLOYER_EOA
when running through a proposalforge script,
and therefore, an address with this exact name must exist in the Addresses.json file. Foundry can be leveraged to actually broadcast the deployments when using thebroadcast
flag combined with--account
flag. Please refer to the foundry docs for more information. Alternatively, when proposals are executed throughforge script,
the deployer address is the proposal contract itself.preBuildMock()
: Pre-build mock actions. Such actions can include pranking, etching, etc. Example: In Arbitrum Timelock this method is used to set a newoutBox
forArbitrum bridge
usingvm.store
foundry cheatcode.build()
: This function is where most of the FPS magic happens. It utilizes foundry cheat codes to automatically transform plain solidity code into calldata encoded for the user's governance model. This calldata can then be proposed on the Governance contract, signed by the multisig signers, or scheduled on the Timelock. For instance, an action might involve pointing a proxy to a new implementation address after deploying the implementation in the 'deploy' function as a privileged admin role in the system.The
buildModifier
must be implemented when overriding so that FPS can store the actions to be used later on in the calldata generation. This modifier runs the_startBuild()
function beforebuild()
and theendBuild()
function after. This modifier also takestoPrank
as a parameter, which represents the address used as the caller for the actions in the proposal, such as the multisig address or timelock address.The
startBuild()
function pranks as the caller address, and takes a snapshot of the initial state using Foundry'svm.snapshot()
cheat code. Then, it initiates a call to Foundry'svm.startStateDiffRecording()
cheatcode to record all function calls made after this step. Thebuild()
function is then executed, and all the build steps are recorded. Finally, theendBuild()
function is called which stops the state diff recording, retrieves all the call information, stops the prank and reverts the state to the initial snapshot. It then filters out the calls to only ones made by the specified caller address, ignoring any static calls and calls made to Foundry's Vm contract. This process ensures that only mutative calls made by the caller are filtered and stored in the actions array.For a clearer understanding, take a look at the Bravo proposal example snippet. Here,
build()
has thebuildModifier
, which takes the address ofPROTOCOL_TIMELOCK_BRAVO
as the caller. Multiple calls are made in thebuild()
function, but only the mutative calls made by the caller will be stored in the actions array. In this example, there are three mutative actions. First, the deployed token is whitelisted on the deployed vault. Second, the vault is approved to transfer somebalance
on behalf of the Bravo timelock contract. Third, thebalance
amount of tokens is deposited in the vault from the Bravo timelock contract._validateAction()
: Hook that reviews a single proposal action. In this example, it ensures the action being added to the proposal is not a duplicate. This method can be further overridden to include additional checks for all proposal actions._validateActions()
: This method checks all proposal actions based on user defined checks. It can be overridden to add custom checks on all actions of proposals. As an example, checks performed could be that if a certain function is called, the next three actions should be a predefined set of actions, and if they are not, then revert. In this example, it ensures there is single action in proposal, target is not zero address, args and value cannot be zero and value must be zero if execution chain is L2.getProposalActions()
: Retrieves the sequence of actions for a proposal. This function should not be overridden in most cases. It return targets, values and arguments for all the actions.getCalldata()
: Retrieves any generated governance proposal calldata. This function should not be overridden at the proposal contract level as it is already overridden in the proposal type contract. In this example, it returns propose calldata for the actions.run()
: This function serves as the entry point for proposal execution. It selects theprimaryForkId
which will be used to run the proposal simulation. It executesdeploy()
,afterDeployMock()
,build()
,simulate()
,validate()
,print()
andaddresses.updateJson()
in that order if the flag for a function is set to true.deploy()
is encapsulated in start and stop broadcast. This is done so that contracts can be deployed on-chain.Flags used in
run()
function:DO_DEPLOY: When set to true, triggers the deployment of contracts on-chain. Default value is true.
DO_AFTER_DEPLOY_MOCK: When set to true, initiates post-deployment mocking processes. Used to simulate an action that has not happened yet for testing such as dealing tokens for testing or simulating scenarios after deployment. Default value is true.
DO_BUILD: When set to true, controls the build process and transforms plain solidity code into calldata encoded for the user's governance model. Default value is true.
DO_SIMULATE: When set to true, allows for the simulation of saved actions during the
build
step. Default value is true.DO_VALIDATE: When set to true, validates the system state post-proposal simulation. Default value is true.
DO_PRINT: When set to true, prints proposal description, actions, and calldata. Default value is true.
DO_UPDATE_ADDRESS_JSON: When set to true, updates the
Addresses.json
file with the newly added and changed addresses. Default value is false.
simulate()
: Executes the previously saved actions during thebuild
step. This function's execution depends on the successful execution of thebuild
function.This function can be overridden at the governance-specific contract or the proposal-specific contract, depending on the type of proposal. For example, the Governor Bravo type overrides this function at the governance-specific contract, while the Timelock type overrides it at the proposal-specific contract. In the Governor Bravo example, the proposer address proposes the proposal to the governance contract. It first transfers and delegates governance tokens that meet the minimum proposal threshold and quorum votes to itself. Then it registers the proposal, rolls the proposal to the Active state so that voting can begin, votes yes to the proposal, rolls to the block where the voting period has ended, putting the proposal in the Succeeded state. Then it queues the proposal in the governance Timelock contract, warps to the end of the timelock delay period, and finally executes the proposal, thus simulating the complete proposal on the local fork.
Without calling
build
first, thesimulate
function would be a no-op as there would be no defined actions to execute.validate()
: Validates the system state post-proposal simulation. This allows checking that the contract's variables and newly deployed contracts are set up correctly and that the actions in the build function were applied correctly to the system.This function is overridden at the proposal-specific contract. For example, take a look at the Governor Bravo
validate()
method. It checks the deployment by ensuring the total supply is 10 million and Bravo Timelock is the owner of the deployed token and vault, as the ownership of these contracts was transferred from the deployer EOA to the Bravo Timelock in thedeploy()
method for this proposal. It checks the proposal actions simulation by ensuring that the token was successfully whitelisted on the vault and the vault's token balance is equal to the Bravo Timelock's deposit in the vault.getProposalId()
: Checks and returns proposal id if there are any on-chain proposals that match the proposal calldata. There is no need to override this function at the proposal specific contract as it is already overridden in the governance specific contract. Check Timelock Proposal, Governor Bravo Proposal and OZ Governor Proposal to get implementation details for each proposal type. This function is not implemented for a multisig proposal as it is not possible to check the state of a gnosis safe transaction on chain. This method reverts with "Not implemented" message for multisig proposals.print()
: Print proposal description, actions, and calldata. No need to override.
The actions in FPS are designed to be loosely coupled for flexible implementation, with the exception of the build and simulate functions, which require sequential execution. This design choice offers developers significant flexibility and power in tailoring the system to their specific needs. For example, a developer may choose to only execute the deploy and validate functions, bypassing the others. This could be suitable in situations where only initial deployment and final validation are necessary, without the need for intermediate steps. Alternatively, a developer might opt to simulate a proposal by executing only the build and simulate functions, omitting the deploy step if there is no need to deploy new contracts. FPS empowers developers with the ability to pick and choose functions from a proposal for integration tests, deployment scripts, and governance proposal creation as it becomes easy to access whichever part of a governance proposal that is needed and test it locally in as close to production conditions as possible.
Last updated