Learn
Getting Started
Helium Modular Governance lets you effortlessly DAOs to manage your communities on Solana!
Looking to learn more about launching a DAO, creating proposals, and unleashing your community? Read on.
Initializing the SDK
Every smart contract on Helium Modular Governance comes with an SDK. The six main smart contracts are nft-voter-sdk
, organization
, organization_wallet
, proposal
, state_controller
, and token_voter
.
Let's get started by installing the sdks
yarn add @helium/nft-voter-sdk @helium/organization-sdk @helium/organization-wallet-sdk @helium/proposal-sdk @helium/state-controller-sdk @helium/token-voter-sdk
Note that you only need to install the sdks of the contracts you wish to use.
Now, we can initialize the sdks:
import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { PublicKey } from '@solana/web3.js'
import {
PROGRAM_ID as PROPOSAL_PID,
init as initProposal,
} from '@helium/proposal-sdk'
import { PROGRAM_ID, init, proposalKey } from '@helium/organization-sdk'
const provider = anchor.getProvider()
// Create unique name
const name = `DAO Test ${Math.floor(Math.random() * 1000)}`
const organizationProgram = await init(provider, PROGRAM_ID)
const proposalProgram = await initProposal(provider, PROPOSAL_PID)
Creating and Managing Proposals
An Organization is not required for a proposal. This allows maximum flexibility, builders can create their own hierarchical structures outside of the organization smart contract. Let's create a simple Yes/No proposal.
First, create a proposal config. This tells a proposal how votes will be tallied and how state progression occurs. By setting ourself as the vote and state controller, we control both. Using our wallet, we can add/remove votes and move the proposal from Draft
to Voting
to Resolved
.
const {
pubkeys: { proposalConfig },
} = await proposalProgram.methods
.initializeProposalConfigV0({
name,
voteController: provider.wallet.publicKey,
stateController: provider.wallet.publicKey,
// Set to default pubkey to disable vote hooks
onVoteHook: PublicKey.default,
})
.rpcAndKeys();
Now lets create the proposal:
const {
pubkeys: { proposal },
} = await proposalProgram.methods
.initializeProposalV0({
// Seeds are unique per `namespace`. The `namespace` account defaults to the calling wallet.
// The Organization uses this feature to delineate proposals by a unique `id` index. Ex `namespace, proposal 0`, `namespace, proposal 1`.
// The `namespace` must sign for creating proposals. This prevents malicious collisions
seed: Buffer.from(name, "utf-8"),
// Allows for voters to vote for more than one choice
maxChoicesPerVoter: 1,
name,
// Optionally put additional JSON information about the proposal here.
uri: "https://example.com",
choices: [
{
name: "Yes",
// Optionally put additional JSON information about the choice here. This is useful for long form markdown choices
uri: null,
},
{
name: "No",
uri: null,
},
],
tags: ["test", "tags"],
})
.accounts({ proposalConfig })
.rpcAndKeys();
Notice that every Anchor call only requires a minimum subset of accounts. Modular-governance's smart resolvers remove the pain of account munging! Just specify the accounts that you think are necessary, and it is most likely sufficient.
Proposal Lifecycle
Every proposal has a state
field that starts as a Draft
. In order to vote, a proposal needs to be in state Voting
. When a vote is finished, it will be Resolved
.
The main way to update the state
of a proposal is via the state controller. Let's set this proposal to voting:
await proposalProgram.methods
.updateStateV0({
newState: {
voting: {
// Started voting now
startTs: new anchor.BN((new Date().valueOf()) / 1000),
},
},
})
.accounts({ proposal })
.rpc();
Custom States
You can implement custom states like signoff via the custom state
await proposalProgram.methods
.updateStateV0({
newState: {
custom: {
name: "Sign Off",
bin: Buffer.from([]) // You can include a custom struct here if necessary
},
},
})
.accounts({ proposal })
.rpc();
Voting
Now that the proposal is in the Voting
state, you can vote on a proposal by calling voteV0
await proposalProgram.methods
.voteV0({
// This is voting for choice "No"
choice: 1,
weight: new anchor.BN(2),
// Vote controllers are able to switch this flag to add and remove votes
removeVote: false,
})
// The voter is used for indexing, associating votes with wallets. This is more strictly enforced by vote controllers
.accounts({ proposal, voter: provider.wallet.publicKey })
.rpc();
Organizations
Proposals on their own are powerful, but we need a way to organize them. Organizations are a holding pattern for proposals. They allow you to group and list proposals, as well as keeping a default configuration for proposals.
Creating an Organization
var {
pubkeys: { organization },
} = await organizationProgram.methods.initializeOrganizationV0({
name,
authority: provider.wallet.publicKey,
// Reuse the proposal config from above
defaultProposalConfig: proposalConfig,
// Modular governance does not require you use Helium's deployed programs. You can deploy your own.
proposalProgram: proposalProgram.programId,
// Extra json information about the organization
uri: 'https://example.com',
})
We can fetch the accounts we just created
var acct = await proposalProgram.account.organizationV0.fetch(organization!);
Creating a proposal
Now, we can create a proposal with the default config:
var {
pubkeys: { proposal },
} = await proposalProgram.methods
.initializeProposal({
maxChoicesPerVoter: 1,
name: 'Proposal Test',
uri: 'https://example.com',
choices: [
{
name: 'Yes',
uri: null,
},
{
name: 'No',
uri: null,
},
],
tags: ['test', 'tags'],
})
.accounts({ organization, proposalConfig })
Now we can fetch the proposal we just created:
var acct = await proposalProgram.account.proposalV0.fetch(proposal!);
Voting on a proposal
Now, we can vote on the proposal:
await proposalProgram.methods
.updateStateV0({
newState: {
voting: {
// Started voting now
startTs: new anchor.BN((new Date().valueOf()) / 1000),
},
},
})
.accounts({ proposal })
.rpc();
await proposalProgram.methods
.voteV0({
choice: 1,
weight: new anchor.BN(2),
removeVote: false,
})
.accounts({ proposal, voter: provider.wallet.publicKey })
State Controllers
State controllers are programs that impose rules and structure to proposal state
transition. The default state controller allows boolean logic for determining when a proposal is resolved.
State controllers work in two ways
- The state controller has the authority to update the state of the proposal. This typically happens via calling the
resolveV0
endpoint, which is permissionless. Clockwork can call this endpoint after a vote finishes, for example - Via the on vote hook. If specified in the proposal config,
onVoteV0
is called on the state controller, and the controller can optionally return an early resolution.
Let's take a look at the default state controller.
We can form very complex resolution settings. Let's take a hypothetical governance with early tipping where we want a proposal to resolve to the top choice when either
- 1 week has passed AND the top choice has > 50% of the vote OR
- The top choice has at least 75% of the votes AND at least 100 vote weight.
First, we need to get our resolution settings nodes
. nodes
represent boolean logic to accomplish the above.
import {settings, init, PROGRAM_ID as STATE_CONTROLLER_PROGRAM_ID} from "@helium/state-controller-sdk"
import BN from "bn.js"
const stateControllerProgram = await init(provider);
const nodes = settings().or(
settings().and(
// One week from the start time
settings().offsetFromStartTs(new BN(60 * 60 * 24 * 7)),
settings().and(
settings().choicePercentage(50),
settings().top(1)
)
),
settings().and(
settings().and(
settings().choicePercentage(75),
settings().choiceVoteWeight(new BN(100))
),
// Choice percentage and vote weight resolve eagerly. Without a time bound, they will resolve to `[]` (no choices won)
// immediately. Specifying an `AND` operation with `numResolved` will prevent the vote from resolving this branch
// until the left side of the branch has at least `1` resolved option
settings().numResolved(1)
)
).build()
Next, create the resolution controller
const {
pubkeys: { resolutionSettings },
} = await stateControllerProgram.methods
.initializeResolutionSettingsV0({
name: "Early Tipping 75%, 1 week 50%",
settings: {
nodes,
},
})
.rpcAndKeys()
Next, create a proposal config:
const {
pubkeys: { proposalConfig },
} = await proposalProgram.methods
.initializeProposalConfigV0({
name,
voteController: provider.wallet.publicKey,
stateController: resolutionSettings,
onVoteHook: STATE_CONTROLLER_PROGRAM_ID,
})
.rpcAndKeys();
Next, we can initiate voting via the state controller
await stateControllerProgram.methods
.updateStateV0({
newState: { voting: {} },
})
.accounts({ proposal })
.rpc();
Notice that the voting
state here does not take startTs
. This is by design -- the state controller enforces setting a proper start ts. The state controller also enforces that only the owner
of a proposal can move it from the Draft
state to the Voting
state.
Now, let's eagerly resolve the vote
await proposalProgram.methods
.voteV0({
// This is voting for choice "No"
choice: 1,
weight: new anchor.BN(100),
removeVote: false,
})
.accounts({ proposal, voter: provider.wallet.publicKey })
.rpc();
If we fetch the proposal, we will see it has resolved:
const proposalAcct = await proposalProgram.account.proposalV0.fetch(proposal)
expect(proposalAcct.state.resolved.choices).to.deep.eq([1])
Creating your own State Controller
For more complex workflows and resolution strategies, you may wish to create your own state controller. The only requirement for a state controller is that it uses its authority to update the state when necessary. This could be a wallet owned by a web2 service. Or a new program.
Vote Controllers
Vote controllers translate voting mechanism to vote weights on a proposal. There are many vote mechanisms. 1 NFT = 1 vote (nft-voter
). 1 Token = 1 vote (token-voter
). VeTokens that give more vote weight to longer lockups (helium-program-library/voter-stake-registry
). The opportunities are endless, and so modular governance explicitly doesn't take any opinions. As far as a proposal is concerned, there is an address that signs to set votes for voters.
Let's use an Token voter. The token-voter
contract works by allowing voters to deposit their voting tokens, and in exchange receive an NFT receipt
. This allows the program to track votes while knowing individual tokens cannot move to another account while they are being used in a vote, stopping double votes.
First, configure proposal config and token voter:
import { init } from "@helium/token-voter-sdk";
import { createMint } from "@helium/spl-utils";
const tokenVoterProgram = await init(provider);
const me = provider.wallet.publicKey;
// If you don't already have a mint address
const mint = await createMint(provider, 8, me, me);
const {
pubkeys: { tokenVoter },
} = await program.methods
.initializeTokenVoterV0({
name,
authority: me,
// JSON metadata for the collection of the receipt NFTs
collectionUri: "https://example.com",
})
.preInstructions([
ComputeBudgetProgram.setComputeUnitLimit({ units: 500000 }),
])
.accounts({
mint,
})
.rpcAndKeys()
const {
pubkeys: { proposalConfig },
} = await proposalProgram.methods
.initializeProposalConfigV0({
name,
voteController: tokenVoter,
stateController: provider.wallet.publicKey,
// Set to default pubkey to disable vote hooks
onVoteHook: PublicKey.default,
})
.rpcAndKeys();
Now, create a proposal and set it to voting:
const {
pubkeys: { proposal },
} = await proposalProgram.methods
.initializeProposalV0({
seed: Buffer.from(name, "utf-8"),
maxChoicesPerVoter: 1,
name,
uri: "https://example.com",
choices: [
{
name: "Yes",
uri: null,
},
{
name: "No",
uri: null,
},
],
tags: ["test", "tags"],
})
.accounts({ proposalConfig })
.rpcAndKeys());
await proposalProgram.methods
.updateStateV0({
newState: {
voting: {
startTs: new BN(0),
},
},
})
.accounts({ proposal })
.rpc();
Next, the user deposits their tokens:
import { deposit } from "@helium/vote-controller-sdk";
const {
pubkeys: { receipt },
} = await (
await deposit({
program: voteControllerProgram,
amount: toBN(10, 0),
// The JSON metadata for the receipt NFT.
metadataUri: "https://example.com",
tokenVoter,
})
).rpcAndKeys();
Now vote:
await voteControllerProgram.methods
.voteV0({
choice: 0,
})
.accounts({ receipt, proposal })
.rpcAndKeys()
To withdraw our tokens, we need to relinquish all active votes:
await voteControllerProgram.methods
.relinquishVoteV0({
choice: 0,
})
// Refund specifies a rent refund destination
.accounts({ receipt, proposal, refund: me })
.rpc();
Now we can withdraw:
await voteControllerProgram.methods
.withdrawV0()
.accounts({ receipt, refund: me })
.rpc()
The nft-voter
contract works similarly to the token-voter
, but does not require deposits and withdrawals. Simply initialize nft-voter
with your NFT collection, and issue vote/relinquish vote with any NFT in that collection.
Executable Proposals
A common use case for governance is to execute stored procedures once a proposal passes. This could include treasury management operations, program upgrades, etc.
The organization-wallet
smart contract allows associating multiple wallets with an organization, and executing transactions using that wallet.
First, set up the sdk
import { init, walletKey, compileTransaction, executeTransaction } from "@helium/organization-wallet-sdk";
const organizationWalletProgram = await init(provider)
Next, let's create a wallet:
const {
pubkeys: { organizationWallet },
} = await organizationWalletProgram.methods
.initializeOrganizationWalletV0({
index: 0,
name: "First Org Wallet",
proposalConfigs: [proposalConfig!],
})
.accounts({
organization,
})
.rpcAndKeys();
// Getting the wallet address for this org wallet
const wallet = walletKey(organization, 0)[0]
While a proposal is in Draft
state, we can add transactions by forming arrays of instructions:
const instructions = [... your logic to for a transaction ...];
const { transaction, remainingAccounts } = await compileTransaction(
instructions,
[] // Optional ephemeral signers. This can be useful for operations like creating mints, which require a signer keypair
);
const {
pubkeys: { choiceTransaction },
} = await organizationWalletProgram.methods
.setTransactionsV0({
// This transaction is associated with choice 0 passing
choiceIndex: 0,
// This is the first transaction on choice 0. There can be multiple txns associated with a choice
transactionIndex: 0,
transaction,
// Time lock. Allow the tx to execute 0 seconds (immediately) after the proposal finishes
allowExecutionOffset: 0,
// Stop allowing execution after 1 week. This prevents malicious stale instructions executing at a much later point
disableExecutionOffset: 60 * 60 * 24 * 7,
})
.remainingAccounts(remainingAccounts)
.accounts({
proposal,
organizationWallet,
})
.rpcAndKeys();
Once the proposal passes and resolves to choice 0, we can execute the transaction:
await (
await executeTransaction({
program: organizationWalletProgram,
choiceTransaction,
})
).rpc()
Next Steps
Interested in using Modular Governance with React? Checkout our React Examples.
To gain a deeper understanding of the API, check out the API pages on