Skip to content

Council Deployment Guide

William Villanueva edited this page Mar 27, 2023 · 6 revisions

If you're interested in learning how to deploy your own version of the Council governance contracts on the blockchain, you've come to the right place.

Through this guide you will learn how to do the following:

  1. Set up the Council deployment scripts
  2. Adjust Council to suit your DAO's needs
  3. Deploy the Council contracts to the blockchain

The Council Kit includes apps and packages that will help you with various tasks around the deployment and usage of the Council governance framework. This guide will focus on how to use the council-deploy package to deploy the Council contracts onto the blockchain.

Requirements

  • Git
  • Node 14+
  • Yarn package manager
  • Etherscan API Key
  • Node provider URI and API Key (e.g. Alchemy)
  • Private Key for an Ethereum address to deploy the contracts

Clone the council-kit repository

The first step is just getting the code. We'll do that by using git to clone the repository from GitHub, so open up your preferred command line terminal and run the following command:

git clone [email protected]:element-fi/council-kit.git

NOTE If you want to make some changes to the codebase and/or eventually deploy your own flavor of the Council Reference UI, you'll probably want to fork the repo on GitHub and clone your fork instead

If you take a look at the directory structure, you'll find that this guide will focus on the packages/council-deploy package.

Fetch Contract Code and Install Dependencies

Run the following command on the root directory council-kit to install the required dependencies:

yarn install

Since we're deploying contracts onto the blockchain, and using the Etherscan API to verify them, we also need to fetch the contract code with the following command:

yarn workspace @council/deploy run load-contracts

Now we can build the project's packages:

yarn build

Finally, don't forget to create a packages/council-deploy/.env file and add your environment variables, namely the Alchemy and Etherscan API keys, and the deployer address private key. You can just use the packages/council-deploy/.env.sample file as a template.

Customizing Council

If you've followed the guide up to this point, you can already deploy a default version of Council by using the packages/council-deploy/src/deployCouncil.ts script (just jump to the Deploying Council Contracts section)

However, chances are that you'll need to make some configuration adjustments to the script for this Council deployment to fit your DAO's structure and needs, such as:

  • Will the DAO use a Governance Token? If so, does it already have one on the blockchain?
  • Will the DAO use a Governance Steering Council?
  • Does the DAO have an existing Treasury contract?
  • Which Voting Vaults will the DAO use?

The Council Kit provides a default implementation for all of these situations. This section will explore some of these configuration options.

Customizing: Governance Token

The first few lines of the deployCouncil.ts script execute the deployment of a standard ERC20 token contract with the given token name and symbol, to be used for Governance through Council:

// The voting token is used to determine voting power in the Locking Vault and
// Vesting Vault. It has no dependencies on any of the council contracts.
const votingToken = await deployVotingToken({
    tokenName: "Council Voting Token",
    tokenSymbol: "CVT",
    signer,
});

Scenario 1: The DAO doesn't have a Governance Token, but wants to use one

In this case, just change the tokenName and tokenSymbol parameters into the DAO's preferred values for their Governance Token. Council's deployment script will handle the token contract's deployment and verification.

Scenario 2: The DAO already has a Governance Token, or wants to use a custom one

If the DAO already has its own Governance token, you can skip this token deployment by removing (or commenting out) the aforementioned lines of code.

You'll also have to adjust the code that depends on the votingToken address, namely the Locking vault and Vesting vault deployments. To do that, change the lines:

votingTokenAddress: votingToken.address

into your Governance token address. For example:

votingTokenAddress: "0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d"

In this example, we've set the votingTokenAddress to Element's ELFI contract, whose address is 0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d

Customizing: The Governance Steering Council (GSC)

The next few lines of code in the deployCouncil.ts script handle the deployment of the GSC Core Voting contract, which gives the Governance Steering Council its privileged permissions:

// The GSC Core Voting is a privileged voting contract
...
const gscCoreVoting = await deployGSCCoreVoting({
  signer,
  ...
    votingVaultAddresses: [],
  ...
    ownerAddress: signer.address, // temporary assignment
  ...
  baseQuorum: "1",
  lockDuration: 10,
  extraVotingTime: 15,
});

It also uses a gscVault to keep track of voting power:

const gscVault = await deployGSCVault({
  signer,
  ownerAddress: timelock.address,
  // GSC vault depends on core voting contract to prove that members meet the
  // voting power minimum to be on the GSC.
  coreVotingAddress: coreVoting.address,
  // any test account can get onto GSC with this much vote power
  votingPowerBound: "100",
  // members are idle for 60 seconds after they join the GSC
  idleDuration: 60,
});

Scenario 1: The DAO wants to have a GSC

In this case, you'll just want to adjust the parameters that control the GSC's privileges in the respective gscCoreVoting and gscVault contracts:

gscCoreVoting:
  • baseQuorum: Controls how many GSC members are needed to pass a proposal. Default value is 1.
  • lockDuration: Controls how many blocks to wait from creation time before a proposal can be executed (provided it has reached quorum and is passing). For reference, 1 week is around 300,000 blocks.
  • extraVotingTime: Controls how long to wait from unlocking time (see lockDuration) before a proposal is closed for voting.
gscVault:
  • votingPowerBound: How much voting power a user needs to be able to join the GSC. This value should be set according to the DAO's token supply plan.
  • idleDuration: This is the amount of time a new member of the GSC must wait before they can do anything as a GSC member.

Scenario 2: The DAO doesn't need a GSC

In this case, you can skip deployment of the gscCoreVoting contract and the gscVault contract:

Adjust references to gscCoreVoting by setting them to the 0x00 address:

const timelock = await deployTimelock({
...
  gscCoreVotingAddress: "0x0000000000000000000000000000000000000000"
...
const coreVoting = await deployCoreVoting({
...
  gscCoreVotingAddress: "0x0000000000000000000000000000000000000000"
...

Remove the following code, which updates some parameters on the gscCoreVoting contract:

await gscCoreVoting.contract.changeVaultStatus(gscVault.address, true);
console.log("Approved GSCVault on GSCCoreVoting");

// Now we transfer ownership to the Timelock, any future upgrades to
// GSCCoreVoting must go through the normal proposal flow.
await gscCoreVoting.contract.setOwner(timelock.address);
console.log("Set owner of GSCCoreVoting to Timelock");

With that, your deployment will not include any functionality related to the GSC.

Customization: The CoreVoting Contract

  • baseQuorum: Controls how much voting power is needed for a proposal to pass. This value should be set according to the DAO's token supply plan.
  • minProposalPower: Controls how much voting power is needed to create a proposal. This value should be set according to the DAO's token supply plan.
  • lockDuration: Controls how many blocks to wait from creation time before a proposal can be executed (provided it has reached quorum and is passing). For reference, 1 week is around 300,000 blocks.
  • extraVotingTime: Controls how long to wait from unlocking time (see lockDuration) before a proposal is closed for voting.

Customization: The Timelock Contract

  • waitTimeInBlocks: This is the amount of blocks that you have to wait before a call can be executed by the Timelock.
  • gscCoreVotingAddress: The GSC has a special permission to increase the time that a proposal must wait before it can be executed. This is a security feature. If your deployment does not include a GSC, you should set this to the 0x00 address.

The Timelock contract is where proposals get executed. It is owned by the CoreVoting Contract. This way, proposals can register calls on the timelock.

At the same time, the Timelock contract owns the CoreVoting contract. As such, the only way to upgrade the CoreVoting contract is through a proposal which gets executed through the timelock.

Customization: The Treasury Contract

If the DAO does not have a treasury contract they want to use with Council, the script will deploy a treasury contract.

However, if the DAO does have its own treasury contract, it doesn't need to deploy a new one, so you can just remove the following code:

const treasury = await deployTreasury({
  signer,
  ownerAddress: timelock.address,
});

It is recommended that Council's timelock contract should be the owner of the Treasury contract, so that any usage of the treasury must go through the proposal process.

Customization: Voting Vaults

The Council Kit includes two default vault implementations: the Locking vault and the Vesting vault. They both require the use of a Governance Token or Voting Token. If the DAO already has its own Governance Token that they'd like to use with Council, make sure to update both vault deployments with the token's contract address by changing:

votingTokenAddress: votingToken.address

into your Governance token address. For example:

votingTokenAddress: "0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d"

In this example, we've set the votingTokenAddress to Element's ELFI contract, whose address is 0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d

The Vesting Vault gives voting power to tokens that are still being vested, but applies a multiplier to the unvested tokens, which allows the DAO to reduce their voting power. This multiplier can be set in the packages/council-deploy/src/vaults/deployVestingVault.ts file, by adding a line to set this value before the timelock address is set:

await vestingVault.initialize(signer.address, signer.address);

// Set the initial unvestedMultiplier value
await vestingVault.changeUnvestedMultiplier(10)

// Only the Timelock can update things like the unvestedMultiplier.
await vestingVault.setTimelock(timelockAddress);

In this example, the line await vestingVault.changeUnvestedMultiplier(10) sets the multiplier to 10%, so vested tokens will have 0.10 as much voting power as unvested tokens.

Deploying the Council Contracts

Contracts are deployed using the private key of the wallet specified in the .env file, see: GOERLI_DEPLOYER_PRIVATE_KEY.

Once all contracts are deployed, each one will be verified on Etherscan automatically. (Don't forget to add your ETHERSCAN_API_KEY to the .env file.)

The entire process takes around 10 minutes. This is because some contracts need to interact with other contracts. It also takes a little extra time for Etherscan to index new contracts before they can be verified.

Goerli: Run the following command:

yarn workspace @council/deploy run goerli:deploy-contracts

Mainnet:

yarn workspace @council/deploy run mainnet:deploy-contracts

This will run the deployCouncil.ts script to deploy all the included contracts:

And with that, you're done. All the required contracts are now deployed on the blockchain, so once you've distributed some tokens, you can start doing some Governance.

You'll find that running the script generated a packages/council-deploy/src/deployments/goerli.deployments.json file which contains data about all the contracts that were deployed, should you need it for future reference.

The next step would be to create a web UI to interact with the Council contracts. Luckily, the Council Kit includes a Reference UI, so you can follow the Reference UI Deployment Guide to get started!

Clone this wiki locally