...
比如,创始人 Alice 开始有了一个想法,于是创建了一个 ADao,这时候成员只有创始人一人,所以他的 SBT 权重为 100%,所有的提案他一个人就可以通过。后来他招募了两个合伙人,Bob 和 Tom,每人占 1/3 的 SBT 权重,所有的提案至少需要 2 个人同意(Dao 投票阈值为 50%的情况),ADao 变成了合伙 Dao。再后来,ADao 发行了一个 Token,安装了 Stake 插件,抵押该 Token 的自动成为 Dao 成员,成员 SBT 根据抵押时长计算,该 Dao 成为一个公共 Dao。
五、整体设计
space:空间,负责跟踪提案、投票的全局设置,其包含多个提案,空间中有一些Meta data(头像,描述,主站信息等),DaoSpace用以负责描述空间信息,每个项目方均可通过调用DaoSpace::create 来创建一个space。
...
language | rust |
---|
...
TODO update
...
DaoRegistry
Dao 注册中心,保存在 0x1 账号下的全局注册表,提供 DaoT → Dao address 的反向索引
代码块 |
---|
module DaoRegistry{ |
...
/// |
...
Global |
...
Dao registry info |
...
|
...
|
...
struct |
...
DaoRegistry |
...
has |
...
key{ |
...
|
...
|
...
|
...
|
...
|
...
next_dao_id: u64, } /// Registry Entry for record the mapping between `DaoT` and `dao_address` struct DaoRegistryEntry<phantom DaoT> has key{ dao_id: u64, dao_address: address, } // This function should call from GenesisDao module public(friend) fun register<DaoT>(dao_address: address): u64; |
...
|
...
|
...
public fun dao_address<DaoT>():address; } |
DaoAccount
提供通用的 Dao 账户创建和管理能力。Dao 账户是对 SignerDelegated 账户的一种再封装,Dao 账户创建后,升级策略自动更新为两阶段升级策略。拥有 DaoAccountCap
即可拥有对该 DaoAccount 的控制权。
代码块 |
---|
module DaoAccount{ weight: u128 /// }DaoAccount struct structDaoAccount SpaceCapabilityhas key{ } // 创建 space,一个账号只能创建一个(是否只能一个账户创建一个,待讨论) dao_address: address, public fun create(signer: &signer, voting_delay: u128, votingsigner_durationcap: u128SignerCapability, meta_data: &vector<u8>): SpaceCapability; // 查询 space upgrade_plan_cap: UpgradePlanCapability, public fun query(broker: address) :} (u128, u128, u128); /// 非托管账号This registercapability can control publicthe Dao funaccount register(signer: &signer, space_broker: address, cap: &SpaceCapability) : NFT<NFTMeta, NFTBody>;struct DaoAccountCap has store, key{ // 非托管账号 unregister public fun unregister(signer dao_address: &signeraddress, nft: NFT<NFTMeta, NFTBody>);} // NFT 添加权重 public fun add_weight(signer: &signer, nft: &mut NFT<NFTMeta, NFTBody>, weight: u128); // NFT 减少权重 /// Create a new Dao Account and return DaoAccountCap /// Dao Account is a delegate account, the `creator` has the `DaoAccountCap` public fun reducecreate_weightaccount(signercreator: &signer, nft): &mutDaoAccountCap; NFT<NFTMeta, NFTBody>, weight: u128); } |
DaoRegistry
Dao 注册中心,保存在 0x1 账号下的全局注册表,提供 DaoT → Dao address 的反向索引
...
breakoutMode | wide |
---|
...
/// Provide a function to create signer with `DaoAccountCap`
public fun dao_signer(cap: &DaoAccountCap): signer;
public fun submit_upgrade_plan(cap: &DaoAccountCap, package_hash: vector<u8>, version:u64, enforced: bool);
} |
GenesisDao
定义 Dao,Dao 的 Capability,Dao 的提案处理流程。
代码块 | ||
---|---|---|
| ||
module GenesisDao { struct Dao has key { dao_id: u64, dao_addressname: vector<u8>, } dao_address: address, ) } next_member_id: u64, public} fun dao_address<DaoT>():address{ *&borrow_global<DaoRegistryEntry<DaoT>>.dao_address } } |
DaoAccount
对 Dao Account 的封装,提供通用的 Dao 账号存储能力,是否需要和 Dao 合并在一起
代码块 | ||
---|---|---|
| ||
module DaoAccount{
//Dao 账号 signer 托管
struct DaoSignerCapability has key {
cap: SignerCapability,
}
// The capability for write data to dao account
struct StorageCapability has drop{
dao_address:address
}
struct DaoUpgradePlanCapability has key{
cap: PackageTxnManager::UpgradePlanCapability,
}
struct StorageItem<V has store> has key{
item: V,
}
public fun create_account(creator: &signer){
let signer_cap = Account::create_delegate_account(creator);
let dao_signer = Account::create_signer_with_cap(&siger_cap);
move_to(&dao_signer, DaoSignerCapability{
cap: signer_cap,
});
PackageTxnManager::update_module_upgrade_strategy(&dao_signer, PackageTxnManager::get_strategy_two_phase(), Some(0));
let upgrade_cap = PackageTxnManager::extract_submit_upgrade_plan_cap(&dao_signer);
move_to(&dao_signer, DaoUpgradePlanCapability{
cap: upgrade_cap,
});
}
public(friend) fun dao_signer(dao_address: address): signer {
let signer_cap = borrow_global<DaoSignerDelegate>(dao_address);
Account::create_signer_with_cap(&signer_cap)
}
public(friend) fun apply_storage_capability(dao_address: address): StorageCapability{
StorageCapability{
dao_address
}
}
public fun move_to<V has store>(cap: &StorageCapability, item: V){
move_to(cap.dao_address, StorageEntry{
item,
})
}
public fun move_from<V has store>(cap: &StorageCapability): V {
let StorageItem{ item } = move_from<StorageItem<V>>(cap.dao_address);
item
}
} |
Dao:代表治理的相关流程,该合约中处理底层Space与Proposal的拼装,将一些权限托管到该合约中,考虑还可以往上拆分,即将执行策略部分拆分到上层合约中。
...
breakoutMode | wide |
---|---|
language | rust |
...
/// Configuration of the DAO.
struct DaoConfig has copy, drop, store {
/// after proposal created, how long use should wait before he can vote (in milliseconds)
voting_delay: u64,
/// how long the voting window is (in milliseconds).
voting_period: u64,
/// the quorum rate to agree on the proposal.
/// if 50% votes needed, then the voting_quorum_rate should be 50.
/// it should between (0, 100].
voting_quorum_rate: u8,
/// how long the proposal should wait before it can be executed (in milliseconds).
min_action_delay: u64,
/// how many STC should be deposited to create a proposal.
min_proposal_deposit: u128,
}
public fun withdraw_token<DaoT: store, PluginT, TokenT: store>(_cap: &DaoWithdrawTokenCap<DaoT, PluginT>, amount: u128): Token<TokenT>;
public fun join_member<DaoT: store, PluginT>(_cap: &DaoMemberCap<DaoT, PluginT>, to_address: address, init_sbt: u128);
}
|
DaoTemplate
每个 DAO 初始化的时候都会根据这个模版生成一个命名为 XDao 的 module,并且初始化插件。
代码块 |
---|
/// Dao 生成模版,
module ${DaoName}Dao{
use StarcoinFramework::GenesisDao;
struct ${DaoName} has store{}
const NAME: vector<u8> = b"${DaoName}";
/// sender should create a DaoAccount before call this entry function.
public(script) fun create_dao(sender: signer, voting_delay: u64,
voting_period: u64,
voting_quorum_rate: u8,
min_action_delay: u64,
min_proposal_deposit: u128,){
let dao_account_cap = DaoAccount::extract_dao_account_cap(&sender);
let config = GenesisDao::new_dao_config(
voting_delay,
voting_period,
voting_quorum_rate,
min_action_delay,
min_proposal_deposit,
);
let dao_root_cap = GenesisDao::create_dao<${DaoName}>(dao_account_cap, *&NAME, ${Name}{}, config);
$for_each plugin in $Plugins{
GenesisDao::install_plugin_with_root_cap<${DaoName}, plugin.Name>(&dao_root_cap, ${plugin.Name}::required_caps());
}
GenesisDao::burn_root_cap(dao_root_cap);
}
} |
Proposal
提案代表一次治理过程,Dao 的成员根据 SBT 的快照来进行投票。
代码块 | ||
---|---|---|
| ||
module GenesisDao { /// Proposal data struct. ///struct 是否需要通过 DaoT 来区分 Proposal,如果不需要,则需要把 DAO id 记录在 proposal 中 /// 但 Action 的泛型无法消除 struct Proposal<phantom DaoT: store, Action: store> has keyProposal has store, copy, drop { /// id of the proposal id: u64, /// creator of the proposal proposer: address, /// when voting begins. start_time: u64, /// when voting ends. end_time: u64, /// count of voters who agree with the proposal for_votes: u128, /// count of voters who're against `yes|no|no_with_veto|abstain` with the proposal against_votes: u128vector<u128>, /// executable after this time. eta: u64, /// after how long, the agreed proposal can be executed. action_delay: u64, /// how many votes to reach to make the proposal pass. quorum_votes: u128, /// proposal action. the block number when submit proposal actionblock_number: Option::Option<Action>u64, ///在原来的 Proposalthe 字段上新增两个字段state root of the block which has the block_num:number u64, // 快照高度 root_hashstate_root: vector<u8>, // 快照根hash } } struct ProposalAction<Action: store> has //Setstore 上也需要用泛型来区分{ struct DaoProposalSet { /// id of the proposal proposal_setid: HashSet<u64u64, DaoProposal> } //初始化过程中分配给To creatorprevent 的spam, capproposals must be structsubmitted RootCapabilitywith hasa key{}deposit struct InstallPluginCapability//TODO hasshould key{support custom Token? } structdeposit: ProposalPluginInfo<PluginConfigToken<STC>, has store> hash key{ /// proposal action. installer: address, //通过 BitSet 的不同位置标识插件的不同权限 capabilities: BitSet, config: Option<PluginConfig, } // DaoWithdrawCapability 是一次性的 capability,不能 store,可以 drop struct DaoWithdrawCapability has drop{ } struct DaoMemberMeta<phontem DaoT> has copy{ user: address, } //这里是否需要 DaoT 来区分不同的 Dao? //如果不区分的话,同一个用户加入多个 Dao 的情况,当前的 IdentifierNFT 无法表达 struct DaoMemberBody<DaoT>{ sbt: Token<DaoT>, } // 执行策略:转账 struct ExecutionTransferStrategy<phontem TokenT> { amount: u128, to: address, } // 直接创建 Dao public fun new_dao( creator: &signer, voting_delay: u64, voting_period: u64, min_action_delay: u64) { //这里 Account 需要提供一个新的方法,允许用一个账号去创建另外一个 Delegate 账号。 let signer_cap = Account::create_delegate_account(&signer); let dao_signer = Account::create_signer_with_cap(&siger_cap); //下面面的操作切换到 dao_signer 身份进行操作 let dao = Dao{..}; move_to(&dao_signer, dao); // 托管 Dao 账号的 SignerCapability 到该合约 move_to(&dao_signer, DaoSignerDelegate{cap: signer_cap}); issue_member_nft(creator,&dao_signer); //初始化 dao 的时候无法一次性完成,需要先把 cap 都存到 creator 账号下 //然后按照 plugin 的方式逐步初始化 } // 将一个账号直接升级为 Dao, sender 会变为 Dao 账号 public fun upgrade_to_dao(sender:signer, ...) { //基本逻辑同上,省去了创建账号的流程 } ///这里好像没办法检测 DaoT 和 dao_address 的关系 public fun register<DaoT>(creator: &signer, dao_address: address){ let dao = borrow_global<Dao>(DaoRegsitry::dao_address<DaoT>()); //check the dao creator and creator args DaoRegistry::register<DaoT>(dao_address, dao.id) } fun dao_signer<DaoT>(): signer { let signer_cap = borrow_global<DaoSignerDelegate>(DaoRegsitry::dao_address<DaoT>()); Account::create_signer_with_cap(&siger_cap) } /// _access_key 确保这个调用来自 PluginT 的 Module public fun install_proposal_plugin<DaoT, PluginT>(cap: &dao::InstallPluginCapability, _access_key: &PluginT, installer:&signer, capabilites: vector<u8>){ let dao_signer = dao_signer<DaoT>(); move_to(&dao_signer, ProposalPluginCapability<PluginT>{ installer: Signer:address_of(installer), capabilites: BiteSet::from_bytes(capabilites), }) } public fun borrow_withdraw_capability<DaoT, PluginT>(plugin: &PluginT): DaoWithdrawCapability { // check capability with ProposalPluginCapability DaoWithdrawCapability } public fun withdraw<DaoT, TokenT>(cap: DaoWithdrawCapability, amount: u64): Token<TokenT>{ let dao_signer = dao_signer<DaoT>(); Account::withdraw<TokenT>(amount) } struct PluginStorageItem<PluginT, V>{ item: V, } public fun move_to<DaoT, PluginT, V>(_access_key: &PluginT, item: V){ let dao_signer = dao_signer<DaoT>(); move_to(&dao_signer, PluginStorageItem<PluginT>{ item }); } // 这里有个难题是 TokenT 从哪里来。 // 一种方法是先生成一个账号,部署合约,然后升级为 dao // 另外一种方法是通过 Dao 的合约升级方式进行部署合约 fun issue_member_nft<DaoT>(creator: &signer, dao_signer: &signer){ Token::register<DaoT>(dao_signer); let basemeta = NFT::new_meta_with_image(); NFT::register_nft_v2<DaoMemberMeta<DaoT>>(dao_signer, basemeta); let creator_address = Signer::address_of(creator); // issue 第一个 NFT 给 creator let meta = DaoMemberMeta<DaoT>{ user: creator; }; //如何初始化 creator 的 sbt? let sbt = Token::zero<DaoT>(); let body = DaoMemberBody<DaoT>{ sbt, } let nft = NFT::mint(basemeta, meta, body); IdentifierNFT::accept<DaoMemberMeta<DaoT>,DaoMemberBody<DaoT>>(creator); IdentifierNFT::grant(dao_signer, creator); } // 参与投票方/创建投票方注册到space public fun register_to_space( signer: &signer, broker: address) { // 创建对应的NFT } // 参与投票方/创建投票方注册到space // 方便再上一层的DAO调用 public fun regiter_to_space_get_nft( signer: &signer, broker: address) : Option::Option<NFT<DaoSpace::NFTMeta, DaoSpace::NFTData>> { } // 根据 space_broker 来创建对应的proposal public fun create_proposal<ExecutionStrategy>( signer: &signer, // 创建者 space_broker: u64, // space 代理人 block_num: u64, // 快照高度 root_hash: vector<u8> // 快照根hash ); // 投票 public fun do_vote( signer: &signer, broker: address, id: u64, amount: u128, choice: u8, proof: &vector<u8>, side_nodes: &vector<u8>) { // 证明... // 取本地NFT // 用NFT投票 do_vote_with_nft(broker, id, choice, ); } // 用NFT来投票 // 方便上层DAO来调用 public fun do_vote_with_nft( broker: address, id: u64, choice: u8, nft: Option::Option<NFT<DaoSpace::NFTMeta, DaoSpace::NFTData>> ) { } } |
DaoTemplate
每个 DAO 初始化的时候都会根据这个模版生成一个命名为 XDao 的 module。需要确认下 XDao 这个 module 是否使用统一的名字更容易使用。
代码块 | ||
---|---|---|
| ||
/// Dao 生成模版,
module {X}Dao{
use StarcoinFramework::Dao;
//XDao 的类型标识
struct X {}
const DAO_ADDRESS = {dao_address};
public(script) init_dao(sender: signer, capabilities:vector<vector<u8>, plugin_configs: vector<vector<u8>>){
let plugin_install_cap = Dao::create_dao();
PluginX::install_plugin(&plugin_install_cap, &sender, capabilities[0],configs[0]);
PluginY::install_plugin(&plugin_install_cap, &sender, capabilities[1],configs[1]);
}
///Dao 的入口方法是不是都需要通过 XDao 模块代理一边?
} |
proposal:提案,即一个提案代表一次投票过程,该部分跟原有的流程类似,但是去掉了TokenType,该合约只处理提案的相关处理过程,不做其他无关的事情。
代码块 | ||||
---|---|---|---|---|
| ||||
// DaoProposal.move
module DaoProposal {
struct ProposalCapability {
proposal_id: u64,
}
struct Proposal<Action> {
... // 现有的一些结构
proposal_id: u64,
voting_system: u8, // 投票类型有单选投票、二次投票、排名投票、加权投票等投票类型
voting_start_time: u64,
voting_end_time: u64,
voting_block_num: u64, // 投票快照高度
voting_block_root: vector<u8>, // 投票快照高度的root hash
action: Option::Option<Action>, // 执行策略的结构参数
}
struct Vote {
/// vote for the proposal under the `proposer`.
proposer: address,
/// proposal id.
id: u64,
choie: u8, // 同意,反对,拒绝
weight: u128, // 投票权重
}
public fun create_proposal<Action>(...,
space_broker: address,
space_id: u64,
root_hash: &vector<u8>): ProposalCapability;
public fun cast_vote(
signer: &signer,
proposer_broker: address,
id: u64,
amount: u128,
choice: u8,
cap: &ProposalCapability
);
public fun extract_strategy<Action>(id: u64) : Action;
};
|
Proposal action plugin
代码块 | ||
---|---|---|
| ||
//所有的 proposal action 插件都需要提供两个公开的 entry script function,参数保持一致 module XProposalPlugin{ use Dao::{Self, ProposalPluginCapability} // XPlugin 的类型标识 struct XPlugin{ } struct XPluginConfig{ cfg_1: u64, } struct XAction{ } struct ProposalPluginCapabilityHolder{ cap: ProposalPluginCapability<XPlugin> } fun new_action(args:vector<u8>): XAction{ //bcs decode args //construct XAction } //返回 plugin 所需要的 capabilities public fun get_capabilities():vector<u8>{} public fun install_plugin<DaoT>(cap: &Dao::PluginInstallCapability, installer: &signer, capabilities: vector<u8>, config:vector<u8>){ //bcs decode config arg, construct config. let config = XPluginConfig{} let plugin = XPlugin{}; Dao::install_proposal_plugin_capability<DaoT, XPlugin>(cap, &plugin, installer, capabilities); //保存配置 Dao::move_to<DaoT, XPlugin>(&plugin, config); } public fun execute_proposal<DaoT>(sender: &signer, proposal_id: u64){ let dao_address = DaoRegistry::dao_address<DaoT>(); let cap_holder = borrow_global<ProposalPluginCapabilityHolder>(dao_address); let actoin = Dao::extract_proposal_action<DaoT, XAction>(&cap_holder.cap, sender, proposal_id); //execute the action,such as trasnfer tokenaction: Action, } } |
Proposal action plugin
以成员加入的 Proposal Plugin 为例,create_proposal
方法会在 DaoT
代表的 Dao 中创建一个 Proposal。该 Dao 的成员进行投票,按照治理流程对 Proposal 进行决策,如果到达可执行状态,则任何人可以调用 execute_proposal
方法执行该提案,执行后,MemberJoinAction
中的 member 成为 Dao 的成员,并且得到了 init_sbt
个 SBT。
代码块 |
---|
module MemberProposalPlugin{ use StarcoinFramework::GenesisDao::{Self, CapType}; use StarcoinFramework::Vector; struct MemberProposalPlugin has drop{} struct MemberJoinAction has store { member: address, init_sbt: u128, } public fun required_caps():vector<CapType>{ Vector::singleton(GenesisDao::member_cap_type()) } public fun create_proposal<DaoT: store>(sender: &signer, member: address, init_sbt: u128, action_delay: u64){ let pluginwitness = XPluginMemberProposalPlugin{}; let withdraw_cap = DaoGenesisDao::borrowacquire_withdrawproposal_capability<DaoTcap<DaoT,XPlugin> MemberProposalPlugin>(&pluginwitness); let tokenaction = Dao::withdraw<STC>(&withdraw_cap, 1000); MemberJoinAction{ //updatemember, the executor } //the entry scriptinit_sbt, function public(script) fun execute_proposal_entry<DaoT>(sender: signer, proposal_id: u64){ }; execute_proposal<DaoT>GenesisDao::create_proposal(&cap, sender, action, proposalaction_iddelay); } public fun execute_proposal<DaoT: propose<DaoT>store>(sender: &signer, args:vector<u8>, exec_delayproposal_id: u64){ let action = new_action(args); let dao_address = Dao::get_dao_address<DaoT>()let witness = MemberProposalPlugin{}; let proposal_cap_holder = borrow_global<ProposalPluginCapabilityHolder>(dao_addressGenesisDao::acquire_proposal_cap<DaoT, MemberProposalPlugin>(&witness); let MemberJoinAction{member, init_sbt} = DaoGenesisDao::propose<DaoTexecute_proposal<DaoT, MemberProposalPlugin, XAction>MemberJoinAction>(&capproposal_holder.cap, sender, action, exec_delay, exec_delayproposal_id); } public(script) funlet propose_entry<DaoT>(sender:signer, args:vector<u8>, exec_delay: u64){member_cap = GenesisDao::acquire_member_cap<DaoT, MemberProposalPlugin>(&witness); proposeGenesisDao::join_member(&sendermember_cap, argsmember, execinit_delaysbt); } } |
...
TODO 详细描述提案处理流程
六、流程
创建流程
用户 A 发起交易,先创建合约账号 X, A 拥有 X 的控制权。
用户填写配置,选择需要安装的插件,生成 用户通过 DApp 界面填写配置,选择需要安装的插件,DApp 根据 Dao Template 生成 XDao module,以及初始化的参数。
编译 浏览器编译 XDao module,并且通过两阶段升级的方式,由 A 来发期交易,将 来发起交易,将 XDao module 部署到 X 账号下,并通 init_script 初始化 XDao 以及插件。
Dao 创建完成。创建完成,进入治理阶段。
...
七、问题
Move实现层面,区分不同的项目方来创建DAO,是通过Template Type 来对其进行异化还是通过 address+id的方式,若按照前者,如何解决合约动态部署的问题?
这个当前有两个方案,这个 issue 中提了两个方案 [Feature Request] Support Issue a Token on webpage · Issue #4 · starcoinorg/dapps (http://github.com ) ,未来可以支持合约中部署合约。Snapshot中,存在不同的准入策略,即参与/创建投票的人在DAO中创建/参与投票的时候会有一个准入的门槛,这个准入的门槛是一系列判断条件,这个条件有预置的也有用户。在solidity中可以动态调用,故可以通过一个ABI的字符数组来进行表示,实际执行的时候也是动态去调用这个ABI,且这些策略是可以拼接的。而Move中如果也按照这个思路显然无法实现
感觉可以简化一下,小额抵押某种 Token 即可,如果是发垃圾投票就通过投票没收。类似的,Snapshot也存在不同的Execution Action,在Move中也可以通过预置Execution的模式去实现,是否还有其他的实现思路?
当前 DAO 的 Action 是支持第三方扩展的,并不是只能用预置的 Action。
执行在前端触发。治理权重的计算(基于长线hodl:锁仓时长、锁仓量),基于余额的权重计算有难度。锁仓有激励。
不同类型项目采用不同机制。Swap自实现weight维护。
扩展点的设计,包括Action等动态调用的扩展,通过interface形式,前端来触发。DAO的平滑升级如何扩展?
Action的扩展性;
DAO类型的扩展性,不同DAO类型如何转换;
投票权重的扩展性;
投票策略类的扩展性;
6.2号讨论:
插件的扩展性,DAO的创建,通过Templete来实现,第一期不开放注册入口
DAO的升级,不同类型的DAO进行抽象,尽可能抽象出同样的类型和权限,否则升级会比较困难;
Member加入的门槛,通过判断某种token的balance,不跟stake耦合;member列表如何存储?
SBT 权重的维护,由项目方合约来更新,无法实现hook机制
一期不提供合约,实现链下DAO+问卷类投票,纯粹API+前端来实现,同步开发合约;二期上线合约版DAO,支持链上DAO和治理,避免合约升级和兼容性问题。
设计通用的Stake流程和Library,锁仓时长、收益、权益的发放等
DAO产品调研和需求完善
论坛/文档
直播
公链升级切换到DAO上
DAO合约分工
...
DAO合约框架 @jolestar
...
插件机制,包括插件权限等 @jolestar
...
DAO Bob
...
Member及 插件机制 Ellen
SBT/NFT
...
Proposal ElementX
...
Action
工具链
...
bsc序列化,参数传参
...
merkle proof
...
五、参考
...