In this post I will be walking through a DAO contract on Soroban.
What Are DAOs?
According to Ethreum.org: "A DAO is a collectively-owned, blockchain-governed organization working towards a shared mission."
DAOs leverage smart-contracts to allow shared decision making and shared ownership of wealth.
I previously wrote a DAO contract that uses reputation to determing voting power and membership. However DAOs normally use a token as a way to determine membership, and the more tokens you have, the more voting power you have.
Soroban DAO
The DAO contract can be found here: https://github.com/rahimklaber/soroban_token_dao, and consists of the DAO contract and a modified token contract.
Note: For usage examples, see https://github.com/rahimklaber/soroban_token_dao/blob/master/dao/src/test.rs
DAO Contract
pub trait DaoTrait {
fn init(
env: Env,
dao_token_id: BytesN<32>,
min_prop_duration: u32,
min_quorum_percent: u32,
min_prop_power: i128,
);
//create proposal and return its id
fn c_prop(env: Env, from: Address, proposal: Proposal) -> u32;
//try to execute prop
fn execute(env: Env, prop_id: u32);
fn proposal(env: Env, prop_id: u32) -> ProposalExtra;
//allow a member to vote on a proposal]
fn vote_for(env: Env, from: Address, prop_id: u32);
fn v_against(env: Env, from: Address, prop_id: u32);
fn v_abstain(env: Env, from: Address, prop_id: u32);
fn votes(env: Env, prop_id: u32) -> VotesCount;
//min power to propose
fn min_prop_p(env: Env) -> i128;
// get minimum duration of proposal
fn min_dur(env: Env) -> u32;
//minimum percentage to for proposal to pass.
// so for (votes + abstain / total_power) * 100 must be > quorum
fn quorum(env: Env) -> u32;
}
The contract consists of a few settings that can be set at initialization:
-
dao_token_id
: The id of the token used to determine voting power. The DAO contract should be set as the admin of the token. -
min_prop_duration
: The minimum duration of a proposal in seconds. -
min_quorum_percent
: The minimum percentage of votes that is needed for a proposal to pass. -
min_prop_power
: The minimum amount of tokens needed to submit a proposal.
Proposal
Someone with a voting power of atleast min_prop_power
can submit a proposal. a proposal, which other can vote on.
#[contracttype]
#[derive(Clone, Debug)]
pub struct ProposalInstr {
//contract id
pub c_id: BytesN<32>,
pub fun_name: Symbol,
pub args: Vec<RawVal>,
}
#[contracttype]
#[derive(Clone, Debug)]
pub struct Proposal {
pub end_time: u64,
// instrunctions will be executed in sequence
pub instr: Vec<ProposalInstr>,
}
A proposal consists of:
-
end_time
: The time in unix seconds after which a proposal cannot be voted for and can be executed. -
instr
: Instructions that would be executed in the case that a proposal is executed.
instr
contains the arguments needed to do a cross-contract call, and would be used like this: env.invoke_contract(&instr.c_id, &instr.fun_name, instr.args)
.
Voting
Users can vote with the vote functions:
-
vote_for
: Vote for the proposal. -
vote_against
: Vote against the proposal. -
v_abstain
: Register the fact that you are abstaining. This can be helpfull if there is a minimum participation requirement. Or if you would like to show participation, but do not have a preference.
DAO Token
The DAO token is a modified version of the Example Token contract from the soroban docs: https://soroban.stellar.org/docs/how-to-guides/tokens.
The token was modified by adding functionality such that it can be used with the DAO contract.
pub trait DaoExtensionTrait {
// Get voting power of a Address
fn power(env: Env, of: Address) -> i128;
fn power_at(env: Env, of: Address, at_block: u32) -> i128;
// delegate power `from` to `to`
fn delegate(env: Env, from: Address, to: Address, amount: i128);
// remove delegation
// amount is the amount we want to remove
// from is the person who delegated
fn r_delegate(env: Env, from: Address, to: Address, amount: i128);
//get amount that `from` has delegated to `to`
fn get_d_a(env: Env, from: Address, to: Address) -> i128;
}
I added a file which contains the DaoExtensionTrait
trait. Implementing the the trait and annotating the implementation with #[contractimpl]
means that the DAO code will end up in the final WASM next to the normal token code.
The extra functions have only 2 uses. Being able to know someones voting power, and being able to delegate power to someone.
There are a few things to keep in mind when voting:
- Before voting, a user must have power delegated to them. They can either delegate to themselves or ask someone else to delegate their tokens. This can be done with the
delegate
function. To reduce complexity of the contract, users must explicitly delegate their tokens to themselves. - A user can only vote using the delegated tokens that they had before the proposal was created. Without this, a user would be able to vote, remove the delegation and add the delegation to another account and vote again. The
power_at
function is used to get the power of an address at the time that the proposal was created.
Possible Improvements
Currently the contracts are not as good as they could be.
Here is a list of things to change or to consider when making your own DAO contract:
- Currently there is no way to change the DAO contract settings. This includes things like the
min_prop_duration
ormin_prop_power
. This file can be referenced when trying to implement this. In short, you have to check when the proposal is attempting to execute a function of the DAO contract and then handle that. -
min_quorum_percent
does nothing. For it to work, the current token supply must be known. However, this is not exposed in the Soroban token specification. This can implemented by modifying the Token contract or by tracking the value in the DAO contract. This would work if only the DAO contract is able to mint the tokens. - A proposal currently contains no metadata. For example, it could be usefull to have a description that explains the proposal. This could be implemented by uploading a document to IPFS and the referencing the IPFS hash of the document when creating the proposal.
Top comments (0)