Creating a Governance Proposal for Berachain Reward Vaults
Black Bera
Black Bera
19 min read
Manny Bera
Manny Bera
19 min read
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.

$HONEY-WBTC Reward Vault

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.

  1. 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.
  2. 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.
  3. 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.

Lifecycle of a Berachain Governance Proposal

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.

Example of Pool Creation on BEX

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:

  1. Create a Rewards Vault for our LP token
  2. Submitting a proposal to Berachain Governance for our new Rewards Vault to be added to BeraChef
  3. Vote on our Rewards Vault
  4. 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 own
  • PRIVATE_KEY - your wallet with BGT
  • FACTORY_ADDRESS - the bArtio Rewards Vault Factory Address
  • LP_TOKEN_ADDRESS - the address of your LP token
  • GOVERNANCE_ADDRESS - Berachain's Governance contract, which has exclusive rights to call BeraChef's updateFriendsOfTheChef
  • 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.

Output of node governance.js --create-vault command

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:

Output of the node governance.js --create-proposal command

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;
    }
}

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 by 1).
  • 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;

        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:

  1. Pending: This is the initial state. This stage takes three hours and you cannot vote or execute your proposal in this stage.
  2. Active: This is the voting period. Cast your vote by running:
# FROM: ./berachain-rewards-vault

node governance.js --vote
  1. Succeeded: The proposal has passed the voting period. It has now been queued for execution.
  2. 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
  1. 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.

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.