Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quitting the DAO #67

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Binary file modified sputnikdao2/res/sputnikdao2.wasm
Binary file not shown.
28 changes: 28 additions & 0 deletions sputnikdao2/src/basic_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::*;
use near_sdk::{env, near_bindgen, AccountId};

#[near_bindgen]
impl Contract {
/// Removes a user from all roles.
/// Returns `true` if the user was removed from at least one role.
///
/// The required parameters are used for checking against the caller
/// and the DAO's name to help in preventing mistakes.
pub fn quit_from_all_roles(&mut self, user: AccountId, dao: String) -> bool {
let quitting_member = env::predecessor_account_id();
if quitting_member != user {
env::panic_str("ERR_QUIT_WRONG_ACC");
}

let dao_name = self.get_config().name;
if dao_name != dao {
env::panic_str("ERR_QUIT_WRONG_DAO");
}

let mut new_policy = self.policy.get().unwrap().to_policy();
let removed = new_policy.remove_member_from_all_roles(&quitting_member);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swfsql I think it should check that this DAO wouldn't lock, like if this was the last council / etc. Would be sad to have a user quit and lock the DAO for scenarios with diff groups or token weights that leave a community hanging.

self.policy
.set(&crate::VersionedPolicy::Current(new_policy));
removed
}
}
5 changes: 4 additions & 1 deletion sputnikdao2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ use near_sdk::{
};

use crate::bounties::{Bounty, BountyClaim, VersionedBounty};
pub use crate::policy::{Policy, RoleKind, RolePermission, VersionedPolicy, VotePolicy};
pub use crate::policy::{
Policy, ProposalPermission, RoleKind, RolePermission, VersionedPolicy, VotePolicy,
};
use crate::proposals::VersionedProposal;
pub use crate::proposals::{Proposal, ProposalInput, ProposalKind, ProposalStatus};
pub use crate::types::{Action, Config};

mod basic_action;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit pick here - but can this file be called "council" or something, so its a file dedicated to council actions/abilities/controls?

mod bounties;
mod delegation;
mod policy;
Expand Down
42 changes: 29 additions & 13 deletions sputnikdao2/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,19 @@ pub struct RolePermission {
pub name: String,
/// Kind of the role: defines which users this permissions apply.
pub kind: RoleKind,
/// Set of actions on which proposals that this role is allowed to execute.
/// <proposal_kind>:<action>
pub permissions: HashSet<String>,
/// Set of proposal actions (on certain kinds of proposals) that this
/// role allow it's members to execute.
/// <proposal_kind>:<proposal_action>
pub permissions: HashSet<ProposalPermission>,
/// For each proposal kind, defines voting policy.
pub vote_policy: HashMap<String, VotePolicy>,
}

/// Set of proposal actions (on certain kinds of proposals) that a
/// role allow it's members to execute.
/// <proposal_kind>:<proposal_action>
pub type ProposalPermission = String;

pub struct UserInfo {
pub account_id: AccountId,
pub amount: Balance,
Expand Down Expand Up @@ -269,8 +275,21 @@ impl Policy {
env::log_str(&format!("ERR_ROLE_NOT_FOUND:{}", role));
}

/// Returns set of roles that this user is member of permissions for given user across all the roles it's member of.
fn get_user_roles(&self, user: UserInfo) -> HashMap<String, &HashSet<String>> {
/// Removes `member_id` from all roles.
/// Returns `true` if the member was removed from at least one role.
pub fn remove_member_from_all_roles(&mut self, member_id: &AccountId) -> bool {
let mut removed = false;
for role in self.roles.iter_mut() {
if let RoleKind::Group(ref mut members) = role.kind {
removed |= members.remove(member_id);
};
}
removed
}

/// Returns a set of role names (with the role's permissions) that this
/// user is a member of.
fn get_user_roles(&self, user: UserInfo) -> HashMap<String, &HashSet<ProposalPermission>> {
let mut roles = HashMap::default();
for role in self.roles.iter() {
if role.kind.match_user(&user) {
Expand All @@ -290,16 +309,14 @@ impl Policy {
) -> (Vec<String>, bool) {
let roles = self.get_user_roles(user);
let mut allowed = false;
let proposal_kind = proposal_kind.to_policy_label();
let action = action.to_policy_label();
let allowed_roles = roles
.into_iter()
.filter_map(|(role, permissions)| {
let allowed_role = permissions.contains(&format!(
"{}:{}",
proposal_kind.to_policy_label(),
action.to_policy_label()
)) || permissions
.contains(&format!("{}:*", proposal_kind.to_policy_label()))
|| permissions.contains(&format!("*:{}", action.to_policy_label()))
let allowed_role = permissions.contains(&format!("{}:{}", proposal_kind, action))
|| permissions.contains(&format!("{}:*", proposal_kind))
|| permissions.contains(&format!("*:{}", action))
|| permissions.contains("*:*");
allowed = allowed || allowed_role;
if allowed_role {
Expand Down Expand Up @@ -343,7 +360,6 @@ impl Policy {
roles: Vec<String>,
total_supply: Balance,
) -> ProposalStatus {
env::log_str(&format!("{:?}", roles));
assert!(
matches!(
proposal.status,
Expand Down
10 changes: 8 additions & 2 deletions sputnikdao2/src/proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,12 +469,18 @@ impl Contract {
/// Act on given proposal by id, if permissions allow.
/// Memo is logged but not stored in the state. Can be used to leave notes or explain the action.
pub fn act_proposal(&mut self, id: u64, action: Action, memo: Option<String>) {
let mut proposal: Proposal = self.proposals.get(&id).expect("ERR_NO_PROPOSAL").into();
let mut proposal: Proposal = self
.proposals
.get(&id)
.unwrap_or_else(|| env::panic_str("ERR_NO_PROPOSAL"))
.into();
let policy = self.policy.get().unwrap().to_policy();
// Check permissions for the given action.
let (roles, allowed) =
policy.can_execute_action(self.internal_user_info(), &proposal.kind, &action);
assert!(allowed, "ERR_PERMISSION_DENIED");
if !allowed {
env::panic_str("ERR_PERMISSION_DENIED")
}
let sender_id = env::predecessor_account_id();
// Update proposal given action. Returns true if should be updated in storage.
let update = match action {
Expand Down
3 changes: 2 additions & 1 deletion sputnikdao2/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ impl Config {
}
}

/// Set of possible action to take.
/// Set of possible actions that a user may be allowed to take on
/// proposals.
#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum Action {
Expand Down
5 changes: 2 additions & 3 deletions sputnikdao2/tests/test_general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,8 @@ fn test_create_dao_and_use_token() {
.assert_success();
vote(vec![&user3, &user2], &dao, 2);
assert!(!view!(dao.get_staking_contract())
.unwrap_json::<AccountId>()
.as_str()
.is_empty());
.unwrap_json::<Option<AccountId>>()
.is_none());
assert_eq!(
view!(dao.get_proposal(2)).unwrap_json::<Proposal>().status,
ProposalStatus::Approved
Expand Down
Loading