Creating a Governance Proposal for Berachain Reward Vaults
One of the biggest perks Berachain's of Proof-of-Liquidity consensus mechanism is the ability for protocols to bootstrap their liquidity by getting Berachain Governance Tokens ($BGT
) emissions. This process of accumulating $BGT
is done through Reward Vaults, represented by the RewardsVaults
smart contract.
Note: You can see the Berps Rewards Vault Smart Contract in action on our testnet.
The process of $BGT
accumulating in Reward Vaults involves a validator being chosen to propose a block, awarded with $BGT
when they are chosen, and distribute the majority of that $BGT
towards one or more Reward Vaults through a distribution contract called BeraChef. A validator's reward is influenced by the amount of $BGT
that is delegated to them at the time that they are proposing the block.
What can you do with $BGT?
There are 3 main functions for $BGT
, which include burning it for $BERA
, delegating it to a validator to boost their rewards, or you can use it in governance to propose or vote on a proposal.
BEX's Rewards Vault Example
BEX is an AMM protocol and one of Berachain's native PoL dApps which also has multiple Reward Vaults. For every whitelisted pool that a user contributes liquidity to, they receive LP tokens proportional to their total contribution.
For example, a user can add liquidity to the HONEY <> WBTC Pool and receive the $HONEY-WBTC LP token in exchange.
You can then take the LP token and stake it into BEX's $HONEY-WBTC Rewards Vault. While the LP token is staked and validators are directing $BGT
emissions towards the Reward Vault, it will accumulate $BGT
over time. This is how BEX incentivizes users to add liquidity to its protocol.
If you were to leverage BEX for your protocol's token (let's call it $BBT
, Black Bera Token) trading liquidity, and paired it against $WBERA
, users will receive a $BBT-WBERA-LP
token when providing liquidity.
If the LP token has been whitelisted, meaning it has a Reward Vault that can accept that token for staking, it can be eligible for $BGT
rewards, which can bootstrap your protocols's liquidity through PoL.
These Rewards Vaults are created by the RewardsVault Factory, which is a smart contract on Berachain that is responsible for creating a Rewards Vault.
While it's permissionless to create your own RewardsVault, for Validators to emit their $BGT
to your Reward Vault, it must be first submitted and approved as a governance proposal to have your Rewards Vault added to BGT Station. Once the proposal is approved, it makes your Rewards Vault a "friendOfTheChef
", which approves it in a collection of eligible Reward Vaults for validators to emit their $BGT
to. This process is also referred to as "whitelisting".
Understanding The Berachain Governance Process For Whitelisting
To understand what we're going to build, let's cover the governance process for creating a Rewards Vault, creating a proposal, voting on a proposal, and activating the Rewards Vault.
Governance Requirements
Prior to creating a successful proprosal, there are a few requirements and steps that need to be considered.
- Min. 1000
$BGT
- All proposals require a minimum of 1000$BGT
which can either be acquired directly or have others delegate their$BGT
to the proposer. - Delegated/Self-Delegated Voting Power - If the minimum
$BGT
is met, to propose or vote on an existing proposal with personally owned$BGT
, it needs to be delegated to the proposer, even to yourself as the proposer. - Quorum Met - The majority of
yes
/for
votes must meet a quorum of 2 billion, which will likely change closer to mainnet. However, on testnet, you can ask the Berachain team to help you meet this Quorum.
Governance Lifecycle
The following represents the different states a proposal will undergo throughout the governance process timeline.
1. Proposal Created (Pending State)
- Waiting period: 3 hours before becoming active for voting
- The proposal can be cancelled as well in this state.
2. Voting Active (Active State)
- Duration: 3 hours
- BGT holders cast votes
- Must meet quorum: 2 Billion BGT minimum
3. Voting Ends
- Proposal either Succeeds or is Defeated
- If Defeated: Marked as such, new proposal can be created after addressing concerns
4. Timelock (if Succeeded)
- Enters queue with 3 hour timelock delay
5. Execution or Expiration
- Executed: Only by Berachain approved Governance EOA
- Expired: If not executed within the timeframe
*NOTE: Time periods are placeholders and may change closer to mainnet launch
Creating A BEX Pool For Your Rewards Vault
For our tutorial, we're going to create a Rewards Vault that accepts an LP token that allows users who do stake eligible to receive $BGT
emissions. Once this Rewards Vault is created with our LP token, we can submit this Rewards Vault to Governance to add it as a "Friend" to Berachef.
First we're going to create our BEX pool with its staking token.
To create a new pool, you will need to supply your protocol token, in this case a newly created token called $BBT
, and the $WBERA
token to create a custom Pool on BEX.
Once the pool and deposit is made, you should receive an LP token. Any more deposits will result in more LP tokens.
Example Deployed Pool & LP Token
Project Setup For Creating A Rewards Vault & Governance Proposal
Now that we have our $BBT
<> $WBERA
BEX pool, with its respective LP Token ($BBT-WBERA-LP
), we need to create a Rewards Vault before we can create a governanace proposal to add our Rewards Vault to Berachef.
We will do this by creating a project that runs a script to do the following:
- Create a Rewards Vault for our LP token
- Submitting a proposal to Berachain Governance for our new Rewards Vault to be added to BeraChef
- Vote on our Rewards Vault
- Activate our Rewards Vault by adding it to BeraChef
Project Requirements
In order to build the script, please make sure that you have the following installed on your computer and/or meet some of these requirements.
- NVM or NodeJS
v20.16.0
or greater - Wallet with
$BGT
accumulated $BERA
tokens to process transactions
1. Initialize a new JavaScript project
Before we create our Rewards Vault for our LP Token, let's set up our development environment. Open your terminal, and follow these steps to get started:
mkdir berachain-rewards-vault
cd berachain-rewards-vault
npm init -y
2. Install the required dependencies
# FROM: ./berachain-rewards-vault
npm install ethers dotenv yargs
3. Create a .gitignore
file
# FROM: ./berachain-rewards-vault
echo "node_modules\n.env" > .gitignore
4. Set up the ABI files
Create an abi
folder in your project root:
# FROM: ./berachain-rewards-vault
mkdir abi
You'll need to download and add these JSON files to this folder:
ERC20.json
- ABI File: The ABI of your LP Token, which is just a generic ERC20 token.BerachainRewardsVaultFactory.json
- ABI File: The ABI for the Berachain Rewards Vault Factory contract- BerachainGovernance.json - ABI File: The ABI for our Berachain Governance Contract. This is the main contract where we will propose, vote, and execute our governance proposals.
- BGT.json - ABI File: The ABI for
$BGT
- BeraChef.json - ABI File: The ABI for BeraChef, the contract that will whitelist our Rewards Vault for the
$BBT
<>$WBERA
LP token. - BerachainRewardsVault.json - ABI File: The ABI of our Rewards Vault
5. Set up the environment variables
Next we will create a .env
file in the project root and add the following content:
Run this command from the root directory:
cat << EOF > .env
RPC=https://bartio.rpc.berachain.com/
PRIVATE_KEY=your_private_key_here
FACTORY_ADDRESS=0x2B6e40f65D82A0cB98795bC7587a71bfa49fBB2B
LP_TOKEN_ADDRESS=insert_your_lp_token_address_here
GOVERNANCE_ADDRESS=0xE3EDa03401Cf32010a9A9967DaBAEe47ed0E1a0b
BERACHEF_ADDRESS=0xfb81E39E3970076ab2693fA5C45A07Cc724C93c2
BGT_ADDRESS=0xbDa130737BDd9618301681329bF2e46A016ff9Ad
EOF
These are the values we've added.
RPC
- our public RPC or replace with your ownPRIVATE_KEY
- your wallet with BGTFACTORY_ADDRESS
- the bArtio Rewards Vault Factory AddressLP_TOKEN_ADDRESS
- the address of your LP tokenGOVERNANCE_ADDRESS
- Berachain's Governance contract, which has exclusive rights to call BeraChef'supdateFriendsOfTheChef
BERACHEF_ADDRESS
- The address to BeraChef, the contract that stores whitelisted Rewards Vaults & validator preferences.BGT_ADDRESS
- The address of the Berachain Governance Token,
Make sure to replace the PRIVATE_KEY
and LP_TOKEN_ADDRESS
(Example $BBT-WBERA-LP
) with your actual values.
6. Create & Setup the governance script
Create a new file named governance.js
in your project root and add the following code:
File: ./governance.js
// Import required libraries
const ethers = require('ethers');
require('dotenv').config();
// Import ABI (Application Binary Interface) for various contracts
const BeraChefABI = require('./abi/BeraChef.json');
const BerachainGovernanceABI = require('./abi/BerachainGovernance.json');
const BGTABI = require('./abi/BGT.json');
const BerachainRewardsVaultABI = require('./abi/BerachainRewardsVault.json');
const ERC20ABI = require('./abi/ERC20.json');
const BerachainRewardsVaultFactoryABI = require('./abi/BerachainRewardsVaultFactory.json');
// Set up the Ethereum provider using the RPC URL from the .env file
const provider = new ethers.JsonRpcProvider(`${process.env.RPC}`, {
chainId: 80084, // Chain ID for Berachain
name: 'Berachain',
ensAddress: null
});
// Initialize the wallet using the private key from the .env file
let wallet;
try {
wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
} catch (error) {
console.error('Error creating wallet:', error.message);
process.exit(1);
}
// Helper function to create contract instances
function createContract(address, abi, signer) {
return new ethers.Contract(ethers.getAddress(address), abi, signer);
}
// Create instances of various contracts
const governance = createContract(process.env.GOVERNANCE_ADDRESS, BerachainGovernanceABI, wallet);
const beraChef = createContract(process.env.BERACHEF_ADDRESS, BeraChefABI, wallet);
const bgt = createContract(process.env.BGT_ADDRESS, BGTABI, wallet);
const factory = createContract(process.env.FACTORY_ADDRESS, BerachainRewardsVaultFactoryABI, wallet);
const token = createContract(process.env.LP_TOKEN_ADDRESS, ERC20ABI, wallet);
let rewardsVault; // This will be initialized later when creating or retrieving a vault
7. Add helper functions
Next, copy and paste our helper functions into the file.
File: ./governance.js
// Function to check the current state of a proposal
async function checkProposalState(proposalId) {
// Get the numerical state of the proposal
const state = await governance.state(proposalId);
// Define an array of state names corresponding to their numerical values
const stateNames = ['Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'];
// Return both the numerical state and its corresponding name
return { state, stateName: stateNames[state] };
}
// Function to determine the next stage in the governance process
async function getNextStage(currentState) {
// Define the order of stages in the governance process
const stageOrder = ['Pending', 'Active', 'Succeeded', 'Queued', 'Executed'];
// Find the index of the current state in the stage order
const currentIndex = stageOrder.indexOf(currentState);
// Return the next stage if it exists, otherwise return 'End'
return currentIndex < stageOrder.length - 1 ? stageOrder[currentIndex + 1] : 'End';
}
// Function to ensure the user has sufficient voting power to create a proposal
async function ensureSufficientVotingPower() {
// Get the user's BGT balance
const balance = await bgt.balanceOf(wallet.address);
console.log('BGT balance:', balance.toString());
// Check who the current delegatee is for the user's BGT
const currentDelegatee = await bgt.delegates(wallet.address);
console.log('Current delegatee:', currentDelegatee);
// Get the user's current voting power
const votingPower = await governance.getVotes(wallet.address, await provider.getBlockNumber() - 1);
console.log('Your voting power:', votingPower.toString());
// Get the proposal threshold (minimum voting power required to create a proposal)
const proposalThreshold = await governance.proposalThreshold();
console.log('Proposal threshold:', proposalThreshold.toString());
// If voting power is less than the threshold
if (votingPower < proposalThreshold) {
// If BGT is not self-delegated, delegate it to self
if (currentDelegatee !== wallet.address) {
console.log('Delegating all BGT to self...');
await (await bgt.delegate(wallet.address)).wait();
console.log('Delegation complete');
} else {
// If already self-delegated but still not enough voting power
console.log('Already delegated to self, but still not enough voting power');
console.log('Please acquire more BGT tokens to meet the proposal threshold');
return false;
}
}
// Check updated voting power after potential delegation
const updatedVotingPower = await governance.getVotes(wallet.address, await provider.getBlockNumber() - 1);
console.log('Updated voting power:', updatedVotingPower.toString());
// If still not enough voting power, return false
if (updatedVotingPower < proposalThreshold) {
console.log('Voting power is still less than proposal threshold, cannot create proposal');
return false;
}
// Sufficient voting power achieved
return true;
}
// Function to check if a proposal with given parameters already exists
async function checkExistingProposal(targets, values, calldatas, descriptionHash) {
// Generate a proposal ID based on the given parameters
const proposalId = await governance.hashProposal(targets, values, calldatas, descriptionHash);
try {
// Try to get the state of the proposal
const state = await governance.state(proposalId);
// If state is not 3 (Defeated), the proposal exists and is not defeated
return state !== 3;
} catch (error) {
// If the error indicates the proposal doesn't exist, return false
// Otherwise, propagate the error
return error.reason === "GovernorNonexistentProposal(uint256)" ? false : Promise.reject(error);
}
}
8. Add The Code To Create Our Rewards Vault
File: ./governance.js
// Function to get an existing rewards vault or create a new one
async function getOrCreateVault() {
console.log('Checking for existing rewards vault...');
try {
// Check if a vault already exists for the token
const existingVaultAddress = await factory.getVault(process.env.LP_TOKEN_ADDRESS);
// If a vault exists (address is not zero)
if (existingVaultAddress !== ethers.ZeroAddress) {
console.log('A rewards vault already exists for this token.');
console.log(`Existing rewards vault address: ${existingVaultAddress}`);
// Provide instructions to view vault details
console.log('\nTo view details about the existing vault:');
console.log('1. Go to https://bartio.beratrail.io');
console.log(`2. Search for the rewards vault address: ${existingVaultAddress}`);
console.log('3. Look for the "Create Rewards Vault" method in the transaction history');
console.log('\nUsing the existing vault for this operation.');
console.log('\nAdd this rewards vault address to your .env file under REWARDS_VAULT_ADDRESS:');
console.log(`REWARDS_VAULT_ADDRESS=${existingVaultAddress}`);
// Create a contract instance for the existing vault
rewardsVault = new ethers.Contract(existingVaultAddress, BerachainRewardsVaultABI, wallet);
return existingVaultAddress;
}
// If no existing vault, create a new one
console.log('No existing vault found. Creating new rewards vault...');
const tx = await factory.createRewardsVault(process.env.LP_TOKEN_ADDRESS);
console.log('Transaction sent. Waiting for confirmation...');
const receipt = await tx.wait();
console.log('Rewards vault created. Transaction hash:', receipt.transactionHash);
console.log();
// Get the address of the newly created vault
const newVaultAddress = await factory.getVault(process.env.LP_TOKEN_ADDRESS);
console.log('New rewards vault created at:', newVaultAddress);
// Provide instructions to view new vault details
console.log('\nTo view details about the new vault:');
console.log('1. Go to https://bartio.beratrail.io');
console.log(`2. Search for the rewards vault address: ${newVaultAddress}`);
console.log('3. Look for the "Create Rewards Vault" method in the transaction history');
console.log('\nAdd this rewards vault address to your .env file under REWARDS_VAULT_ADDRESS:');
console.log(`REWARDS_VAULT_ADDRESS=${newVaultAddress}`);
// Create a contract instance for the new vault
rewardsVault = new ethers.Contract(newVaultAddress, BerachainRewardsVaultABI, wallet);
return newVaultAddress;
} catch (error) {
console.error('Error getting or creating rewards vault:', error);
throw error;
}
}
// Main function to handle command-line arguments
async function main() {
const args = process.argv.slice(2);
const flag = args[0];
switch (flag) {
case '--create-vault':
// Call getOrCreateVault when the --create-vault flag is used
await getOrCreateVault();
break;
// ... other cases ...
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Once you've added the LP_TOKEN_ADDRESS
& PRIVATE_KEY
into your .env
file, paste this code into governance.js
file and then run the script to create your Rewards Vault:
# FROM: ./berachain-rewards-vault
node governance.js --create-vault
This command will create a new Rewards Vault for our LP token and log the address of the created vault.
File: ./.env
Add this new value to your .env file
REWARDS_VAULT_ADDRESS=value from terminal
Note: Your Rewards Vault address will be different from the one seen in this screenshot.
Submitting Your Governance Proposal
Now that we've created our Rewards Vault, it's time to submit a governance proposal to whitelist it. We'll create a new script for this process.
1. Set up the environment
At this point in the process your .env
file should have the following values:
File: ./.env
RPC=https://bartio.rpc.berachain.com/
PRIVATE_KEY=your_private_key
FACTORY_ADDRESS=0x2B6e40f65D82A0cB98795bC7587a71bfa49fBB2B
LP_TOKEN_ADDRESS=your_lp_token_address
GOVERNANCE_ADDRESS=0xE3EDa03401Cf32010a9A9967DaBAEe47ed0E1a0b
BERACHEF_ADDRESS=0xfb81E39E3970076ab2693fA5C45A07Cc724C93c2
BGT_ADDRESS=0xbDa130737BDd9618301681329bF2e46A016ff9Ad
REWARDS_VAULT_ADDRESS=your_rewards_vault_address
2. Create the proposal functions
Now, in the same file, governance.js
, you will add this function below the function getOrCreateVault
File: ./governance.js
async function getOrCreateVault() {
// previous function we implemented
// ...
}
//🚨COPY THIS FUNCTION🚨
async function createProposal(targets, values, calldatas, description) {
// Generate a hash of the proposal description
const hash = ethers.id(description);
// Check if a proposal with these parameters already exists
const proposalExists = await checkExistingProposal(targets, values, calldatas, hash);
if (proposalExists) {
// If the proposal exists, get its ID
const proposalId = await governance.hashProposal(targets, values, calldatas, hash);
// Check the current state of the existing proposal
const { stateName } = await checkProposalState(proposalId);
// Determine the next stage in the proposal process
const nextStage = await getNextStage(stateName);
// Log information about the existing proposal
console.log('\nA proposal with these parameters already exists.');
console.log(`Proposal ID: ${proposalId.toString()}`);
console.log(`Current state: ${stateName}`);
// Inform about the next stage or if it's the final stage
if (nextStage !== 'End') {
console.log(`Next stage: ${nextStage}`);
} else {
console.log('This is the final stage of the proposal.');
}
// Provide instructions to add the proposal ID to the .env file
console.log('\nAdd this proposal ID to your .env file under PROPOSAL_ID:');
console.log(`PROPOSAL_ID=${proposalId.toString()}`);
return proposalId.toString();
}
try {
// If no existing proposal, create a new one
console.log('Creating new proposal...');
const tx = await governance.propose(targets, values, calldatas, description);
const receipt = await tx.wait();
console.log('Proposal transaction confirmed. Transaction hash:', receipt.transactionHash);
console.log();
// Get the ID of the newly created proposal
const proposalId = await governance.hashProposal(targets, values, calldatas, hash);
console.log('New proposal created with ID:', proposalId.toString());
// Provide instructions to add the new proposal ID to the .env file
console.log('\nAdd this proposal ID to your .env file under PROPOSAL_ID:');
console.log(`PROPOSAL_ID=${proposalId.toString()}`);
return proposalId.toString();
} catch (error) {
// Handle any errors that occur during proposal creation
console.error('Error creating proposal:', error);
if (error.error?.data) {
try {
console.error('Decoded error:', governance.interface.parseError(error.error.data));
} catch (parseError) {
console.error('Could not parse error. Raw error data:', error.error.data);
}
}
throw error;
}
}
3. Add command to main function
Next, add the following switch case to the main
function in your governance.js
file, right after the --create-vault
case. This will handle the creation of a governance proposal:
File: ./governance.js
async function main() {
// Get command-line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
// The first argument is our flag/command
const flag = args[0];
switch (flag) {
case '--create-vault':
// If the flag is to create a vault, call the getOrCreateVault function
await getOrCreateVault();
break;
//🚨COPY THIS CASE🚨
case '--create-proposal':
// Check if the user has sufficient voting power to create a proposal
if (!(await ensureSufficientVotingPower())) return;
// Get or create a rewards vault
const vaultAddress = await getOrCreateVault();
// Get the address of the BeraChef contract
const beraChefAddress = await beraChef.getAddress();
// Set up the proposal parameters
const targets = [beraChefAddress]; // The contract to call
const values = [0]; // No BERA being sent with the call
// Encode the function call to updateFriendsOfTheChef
const calldatas = [beraChef.interface.encodeFunctionData('updateFriendsOfTheChef', [vaultAddress, true])];
const description = "Update friends of the chef"; // Description of the proposal
// Create the proposal with the specified parameters
await createProposal(targets, values, calldatas, description);
break;
//🚨END COPY HERE🚨
}
}
This new switch case in the main
function allows you to create a governance proposal to update the "friends of the chef" with your newly created Rewards Vault. It checks for sufficient voting power, retrieves (or creates) the vault address, and then calls the createProposal
function with the necessary parameters.
4. Run the create-proposal command
Now that you've added the --create-proposal
case to your main
function, you can run the command to create your governance proposal to get your Rewards Vault added to BeraChef. Execute the following in your terminal:
# FROM: ./berachain-rewards-vault
node governance.js --create-proposal
This command will create a new governance proposal to whitelist your Rewards Vault. The script will output a proposal ID. You'll need to add this ID to your .env
file for future steps.
After running the command, you should see an output similar to this:
Follow the instructions in the output to update your .env
file. Add a new line with the PROPOSAL_ID
value:
File: ./.env
PROPOSAL_ID=your_proposal_id
This PROPOSAL_ID
will be used in subsequent steps of the governance process, such as voting, queueing, and executing the proposal. It represents the ID of your governance proposal to add your Vault to BeraChef.
5. Create the functions to vote & execute
Add these functions to your governance.js
file beneath the createProposal
function:
File: ./governance.js
// Function to cast a vote on a proposal
async function castVote(proposalId) {
// Check if the wallet has already voted
const hasVoted = await governance.hasVoted(proposalId, wallet.address);
if (hasVoted) {
console.log('Vote already cast for this proposal. Proceeding to next steps.');
return;
}
console.log('Casting vote...');
try {
// Cast a vote in favor of the proposal (1 = yes)
const voteTx = await governance.castVote(proposalId, 1);
const receipt = await voteTx.wait();
console.log('Vote cast successfully. Transaction hash:', receipt.transactionHash);
} catch (error) {
console.error('Error casting vote:', error);
if (error.error?.data) {
try {
console.error('Decoded error:', governance.interface.parseError(error.error.data));
} catch (parseError) {
console.error('Could not parse error. Raw error data:', error.error.data);
}
}
throw error;
}
}
// Function to execute a queued proposal
async function executeProposal(proposalId) {
console.log('Executing proposal...');
try {
const executeTx = await governance.execute(proposalId);
const receipt = await executeTx.wait();
console.log('Proposal executed successfully. Transaction hash:', receipt.transactionHash);
} catch (error) {
console.error('Error executing proposal:', error);
throw error;
}
}
// Function to cancel a proposal
async function cancelProposal(proposalId) {
console.log('Cancelling proposal...');
try {
const cancelTx = await governance.cancel(proposalId);
const receipt = await cancelTx.wait();
console.log('Proposal cancelled successfully. Transaction hash:', receipt.transactionHash);
} catch (error) {
console.error('Error cancelling proposal:', error);
if (error.error?.data) {
try {
console.error('Decoded error:', governance.interface.parseError(error.error.data));
} catch (parseError) {
console.error('Could not parse error. Raw error data:', error.error.data);
}
}
throw error;
}
}
async function whitelistIncentiveToken(tokenAddress, minIncentiveRate) {
console.log('Whitelisting incentive token...');
try {
if (!process.env.REWARDS_VAULT_ADDRESS) {
console.error('Please set the REWARDS_VAULT_ADDRESS in your .env file');
return;
}
const vaultAddress = process.env.REWARDS_VAULT_ADDRESS;
rewardsVault = new ethers.Contract(vaultAddress, BerachainRewardsVaultABI, wallet);
const targets = [vaultAddress];
const values = [0];
const calldatas = [rewardsVault.interface.encodeFunctionData('whitelistIncentiveToken', [tokenAddress, minIncentiveRate])];
const description = `Whitelist incentive token ${tokenAddress}`;
await createProposal(targets, values, calldatas, description);
} catch (error) {
console.error('Error whitelisting incentive token:', error);
throw error;
}
}
These functions handle different stages of the governance process:
castVote
: This function allows you to cast a vote on the proposal. It first checks if you have already voted, and if not, it casts a "yes" vote (represented by1
).executeProposal
: Once a proposal has been queued and the timelock period has passed, this function can be called to execute the proposal. This is the final step in implementing the changes proposed in the governance process.cancelProposal
: You can cancel your proposal before it has been queued to be executed, this is if maybe you filled in the wrong address or description, and would like to create a new one.
Now, replace the main
function in your governance.js
file with the following code:
File: ./governance.js
async function main() {
// Get command-line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
// The first argument is our flag/command
const flag = args[0];
// Get the proposal ID from the environment variables
const proposalId = process.env.PROPOSAL_ID;
switch (flag) {
case '--create-vault':
// Create or retrieve an existing rewards vault
await getOrCreateVault();
break;
case '--create-proposal':
// Check if there's an existing proposal
if (proposalId) {
const { stateName } = await checkProposalState(proposalId);
// Only allow creating a new proposal if the current one is defeated
if (stateName !== 'Defeated') {
console.log(`A proposal (ID: ${proposalId}) already exists and is in ${stateName} state.`);
console.log('You can only create a new proposal if the current one is defeated.');
return;
}
}
// Ensure the user has enough voting power to create a proposal
if (!(await ensureSufficientVotingPower())) return;
// Get or create a rewards vault
const vaultAddress = await getOrCreateVault();
// Get the BeraChef contract address
const beraChefAddress = await beraChef.getAddress();
// Set up proposal parameters
const targets = [beraChefAddress];
const values = [0];
const calldatas = [beraChef.interface.encodeFunctionData('updateFriendsOfTheChef', [vaultAddress, true])];
const description = "Update friends of the chef";
// Create the proposal
await createProposal(targets, values, calldatas, description);
break;
case '--vote':
// Ensure a proposal ID is set
if (!proposalId) {
console.error('Please set the PROPOSAL_ID in your .env file');
return;
}
// Check the current state of the proposal
const voteState = await checkProposalState(proposalId);
// Only allow voting if the proposal is in the Active state
if (voteState.stateName !== 'Active') {
console.log(`Proposal is in ${voteState.stateName} state. Please wait until it reaches Active state to vote.`);
return;
}
// Cast a vote on the proposal
await castVote(proposalId);
break;
case '--execute':
// Ensure a proposal ID is set
if (!proposalId) {
console.error('Please set the PROPOSAL_ID in your .env file');
return;
}
// Check the current state of the proposal
const executeState = await checkProposalState(proposalId);
// Only allow execution if the proposal is queued
if (executeState.stateName !== 'Queued') {
console.log(`Proposal is in ${executeState.stateName} state. Please wait until it reaches Queued state to execute.`);
return;
}
// Execute the proposal
await executeProposal(proposalId);
break;
case '--check-state':
// Ensure a proposal ID is set
if (!proposalId) {
console.error('Please set the PROPOSAL_ID in your .env file');
return;
}
// Check and display the current state of the proposal
const { stateName } = await checkProposalState(proposalId);
console.log(`Current proposal state: ${stateName}`);
// Get and display the next stage of the proposal
const nextStage = await getNextStage(stateName);
if (nextStage !== 'End') {
console.log(`Next stage: ${nextStage}`);
} else {
console.log('This is the final stage of the proposal.');
}
break;
case '--cancel':
// Ensure a proposal ID is set
if (!proposalId) {
console.error('Please set the PROPOSAL_ID in your .env file');
return;
}
// Check the current state of the proposal
const cancelState = await checkProposalState(proposalId);
// Allow cancellation if the proposal is in a cancellable state
if (!['Pending', 'Active', 'Succeeded'].includes(cancelState.stateName)) {
console.log(`Proposal is in ${cancelState.stateName} state and cannot be cancelled.`);
return;
}
// Cancel the proposal
await cancelProposal(proposalId);
break;
case '--whitelist-incentive':
if (!process.env.INCENTIVE_TOKEN) {
console.error('Please set the INCENTIVE_TOKEN address in your .env file');
return;
}
if (!process.env.REWARDS_VAULT_ADDRESS) {
console.error('Please set the REWARDS_VAULT_ADDRESS in your .env file');
return;
}
// Ensure the user has enough voting power to create a proposal
if (!(await ensureSufficientVotingPower())) return;
// Set a default minIncentiveRate (you may want to make this configurable)
const minIncentiveRate = ethers.parseUnits('0.01', 18); // 0.01 token per BGT emission
await whitelistIncentiveToken(process.env.INCENTIVE_TOKEN, minIncentiveRate);
break;
default:
// If an invalid flag is provided, show usage instructions
console.log('Please provide a valid flag:');
console.log('--create-vault: Create a new rewards vault');
console.log('--create-proposal: Create a new governance proposal');
console.log('--vote: Vote on the proposal specified in .env');
console.log('--queue: Queue the proposal specified in .env');
console.log('--execute: Execute the proposal specified in .env');
console.log('--check-state: Check the current state of the proposal');
}
}
// Run the main function and catch any errors
main().catch((error) => {
console.error(error);
process.exit(1);
});
These new cases in the main
function allow you to vote on and execute the governance proposal using the PROPOSAL_ID
from your .env
file. Each case checks if the PROPOSAL_ID
is set before proceeding with the respective action so make sure that your .env
is updated.
Monitor Proposal Progress
Now that you've created your proposal, you need to monitor its progress and take action at the appropriate times. Use the following command to check the current state of your proposal:
# FROM: ./berachain-rewards-vault
node governance.js --check-state
Depending on the state of your proposal, you'll either need to run a command or wait for the next stage in the process:
Pending
: This is the initial state. This stage takes three hours and you cannot vote or execute your proposal in this stage.Active
: This is the voting period. Cast your vote by running:
# FROM: ./berachain-rewards-vault
node governance.js --vote
Succeeded
: The proposal has passed the voting period. It has now been queued for execution.Queued
: The proposal is in the timelock period. Once this period ends, you can execute the proposal, which is when your LP Rewards Vault Will Be officially Added to Berachef:
# FROM: ./berachain-rewards-vault
node governance.js --execute
Executed
: The proposal has been successfully implemented. No further action is needed.
Use the --check-state
command frequently to monitor your proposal's progress.
⚠️ Warning: This process can take several hours due to the waiting periods involved in the governance process. Be patient and keep checking the state.
If at any point your proposal is in the Defeated
or Expired
state, you will need to create a new proposal by running:
# FROM: ./berachain-rewards-vault
node governance.js --create-proposal
Remember to delete your PROPOSAL_ID
in the .env
file if you create a new proposal and add the new one.
Canceling A Proposal
You can as well, cancel a proposal at any stage in the process by running this command.
# FROM: ./berachain-rewards-vault
node governance.js --cancel
This is an irreversible action, however you can create a governance proposal with a new id.
Whitelisting an Incentive Token
After creating your Rewards Vault and getting it approved through governance, you can whitelist additional tokens as incentives. Whitelisting an incentive token allows you to distribute that token alongside $BGT emissions in your Rewards Vault.
To whitelist a token, you'll need to create another governance proposal specifying the token address and a minimum incentive rate. The minimum incentive rate ensures that a meaningful amount of the incentive token is distributed relative to $BGT emissions.
To control the minimum incentive rate for your whitelisted token, you can modify the whitelistIncentiveToken
function in the governance.js
script. Look for the following line:
const minIncentiveRate = ethers.parseUnits('0.01', 18); // 0.01 token per BGT emission
This line sets the minimum incentive rate to 0.01 tokens per BGT emission. You can adjust this value to suit your needs. For example, to set it to 0.1 tokens per BGT emission, you would change it to:
const minIncentiveRate = ethers.parseUnits('0.1', 18); // 0.1 token per BGT emission
To initiate the governance proposal for whitelisting an incentive token once you have adjusted your rates, you can use the following command:
node governance.js --whitelist-incentive
Before running this command, make sure to update your .env
file with the following information:
INCENTIVE_TOKEN
: Set this to the address of the token you want to whitelist as an incentive.REWARDS_VAULT_ADDRESS
: Ensure this is set to the address of your Rewards Vault.
For example, your .env
file should include lines like:
INCENTIVE_TOKEN=0x1234...5678
REWARDS_VAULT_ADDRESS=0xabcd...efgh
Running the command will create a new governance proposal to whitelist your incentive token. Follow the same process as before to monitor, vote on, and execute this proposal to complete the whitelisting process.
Full GitHub Code Repository
If you'd like to see the final code, please take a look at the link below in our Berachain Guides GitHub Repository.
Run the following commands:
git clone https://github.com/berachain/guides.git
cd guides/apps/berachain-governance-proposal
npm install
Follow the README
attached for instructions on downloading and running this script.
What's Next?
🛠️ Want To Build More?
Want to build more on Berachain and see more examples? Take a look at our Berachain GitHub Guides Repo for a wide variety of tutorials.
If you're looking to dive deeper into the details, take a look at our Berachain Docs.
Looking For Dev Support?
Make sure to join our Berachain Discord server and check out our developer channels to ask questions and get support from our community and DevRels.