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 (old) #48

Closed
wants to merge 11 commits into from
31 changes: 31 additions & 0 deletions sputnikdao2/src/basic_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::Contract;
use near_sdk::{env, near_bindgen, AccountId};

#[cfg(not(target_arch = "wasm32"))]
use crate::ContractContract;

#[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);
self.policy
.set(&crate::VersionedPolicy::Current(new_policy));
removed
}
}
1 change: 1 addition & 0 deletions sputnikdao2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::proposals::VersionedProposal;
pub use crate::proposals::{Proposal, ProposalInput, ProposalKind, ProposalStatus};
pub use crate::types::{Action, Config};

mod basic_action;
mod bounties;
mod delegation;
mod policy;
Expand Down
41 changes: 29 additions & 12 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 memeber 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);
mikedotexe marked this conversation as resolved.
Show resolved Hide resolved
};
}
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
2 changes: 1 addition & 1 deletion sputnikdao2/src/proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ impl Contract {
"ERR_PERMISSION_DENIED"
);

// 3. Actually add proposal to the current list of proposals.
// 3. Actually executes or adds proposal to the current list of proposals.
let id = self.last_proposal_id;
self.proposals
.insert(&id, &VersionedProposal::Default(proposal.into()));
Expand Down
3 changes: 2 additions & 1 deletion sputnikdao2/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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
10 changes: 4 additions & 6 deletions sputnikdao2/tests/test_general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,8 @@ fn test_create_dao_and_use_token() {
let staking = setup_staking(&root);

assert!(view!(dao.get_staking_contract())
.unwrap_json::<String>()
.as_str()
.is_empty());
.unwrap_json::<Option<AccountId>>()
.is_none());
add_member_proposal(&root, &dao, user2.account_id.clone()).assert_success();
assert_eq!(view!(dao.get_last_proposal_id()).unwrap_json::<u64>(), 1);
// Voting by user who is not member should fail.
Expand Down Expand Up @@ -124,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
246 changes: 246 additions & 0 deletions sputnikdao2/tests/test_quit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use crate::utils::{add_member_to_role_proposal, add_proposal, setup_dao};
use near_sdk::AccountId;
use near_sdk_sim::{call, to_yocto, view};
use sputnikdao2::{Action, ProposalInput, ProposalKind, VersionedPolicy};
use std::collections::HashMap;

mod utils;

fn user(id: u32) -> AccountId {
format!("user{}", id).parse().unwrap()
}

/// Issue #41 "Quitting the DAO" tests
///
///
#[test]
fn test_quitting_the_dao() {
use near_sdk_sim::UserAccount;
use sputnikdao2::{Policy, RoleKind, RolePermission};
use std::collections::HashSet;

let (root, dao) = setup_dao();
let user2 = root.create_user(user(2), to_yocto("1000"));
let user3 = root.create_user(user(3), to_yocto("1000"));
let user4 = root.create_user(user(4), to_yocto("1000"));

let new_role = |name: String| RolePermission {
name,
kind: RoleKind::Group(HashSet::new()),
permissions: HashSet::new(),
vote_policy: HashMap::new(),
};
let role_none = new_role("has_nobody".to_string());
let role_2 = new_role("has_2".to_string());
let role_3 = new_role("has_3".to_string());
let role_23 = new_role("has_23".to_string());
let role_234 = new_role("has_234".to_string());

let mut policy = view!(dao.get_policy()).unwrap_json::<Policy>();
policy
.roles
.extend(vec![role_none, role_2, role_3, role_23, role_234]);
add_proposal(
&root,
&dao,
ProposalInput {
description: "new_policy".to_string(),
kind: ProposalKind::ChangePolicy {
policy: VersionedPolicy::Current(policy.clone()),
},
},
)
.assert_success();
let change_policy = view!(dao.get_last_proposal_id()).unwrap_json::<u64>();
assert_eq!(change_policy, 1);
call!(
root,
dao.act_proposal(change_policy - 1, Action::VoteApprove, None)
)
.assert_success();

let add_to_roles = |user: &UserAccount, roles: Vec<&str>| {
for role in roles {
add_member_to_role_proposal(&root, &dao, user.account_id.clone(), role.to_string())
.assert_success();

// approval
let proposal = view!(dao.get_last_proposal_id()).unwrap_json::<u64>();
call!(
root,
dao.act_proposal(proposal - 1, Action::VoteApprove, None)
)
.assert_success();
}
};
add_to_roles(&user2, vec!["has_2", "has_23", "has_234"]);
add_to_roles(&user3, vec!["has_3", "has_23", "has_234"]);
add_to_roles(&user4, vec!["has_234"]);

let role_members = |role_permission: &sputnikdao2::RolePermission| -> Vec<AccountId> {
if let RoleKind::Group(ref members) = role_permission.kind {
let mut members = members.into_iter().cloned().collect::<Vec<_>>();
members.sort();
members
} else {
vec![]
}
};

// quits and returns the remaining role names and their members
// this is a Vec so the order is preserved
type RoleNamesAndMembers = Vec<(String, Vec<AccountId>)>;
type RoleNamesAndMembersRef<'a> = Vec<(&'a str, Vec<&'a AccountId>)>;

// return role names and members form a dao
let dao_roles = || -> RoleNamesAndMembers {
view!(dao.get_policy())
.unwrap_json::<Policy>()
.roles
.into_iter()
.map(|role_permission| (role_permission.name.clone(), role_members(&role_permission)))
.collect()
};

// makes references into a RoleNamesAndMembers
// so they are easier to compare against
fn dao_roles_ref(dao_roles: &RoleNamesAndMembers) -> RoleNamesAndMembersRef {
dao_roles
.iter()
.map(|(name, members)| (name.as_str(), members.iter().collect()))
.collect::<Vec<(&str, Vec<&AccountId>)>>()
}

let quit =
|user: &UserAccount, user_check: &UserAccount, dao_name: String| -> Result<bool, String> {
use near_sdk_sim::transaction::ExecutionStatus;
use near_sdk_sim::ExecutionResult;
let res: ExecutionResult = call!(
user,
dao.quit_from_all_roles(user_check.account_id.clone(), dao_name),
deposit = to_yocto("0")
);
match res.status() {
ExecutionStatus::SuccessValue(_bytes) => Ok(res.unwrap_json::<bool>()),
ExecutionStatus::Failure(err) => Err(err.to_string()),
_ => panic!("unexpected status"),
}
};

// initial check,
// when nobody has quit yet
let roles = dao_roles();
{
mikedotexe marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![&user2.account_id,]),
("has_3", vec![&user3.account_id]),
("has_23", vec![&user2.account_id, &user3.account_id]),
(
"has_234",
vec![&user2.account_id, &user3.account_id, &user4.account_id]
)
]
);
}

let config = view!(dao.get_config()).unwrap_json::<sputnikdao2::Config>();
let dao_name = &config.name;

// user2 quits
let res = quit(&user2, &user2, dao_name.clone()).unwrap();
assert!(res);
let roles = dao_roles();
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![]),
("has_3", vec![&user3.account_id]),
("has_23", vec![&user3.account_id]),
("has_234", vec![&user3.account_id, &user4.account_id])
]
);

// user2 quits again
// makes no change
let res = quit(&user2, &user2, dao_name.clone()).unwrap();
assert!(!res);
let roles = dao_roles();
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![]),
("has_3", vec![&user3.account_id]),
("has_23", vec![&user3.account_id]),
("has_234", vec![&user3.account_id, &user4.account_id])
]
);

// user3 quits incorrectly, passing the wrong user name
let res = quit(&user3, &user2, dao_name.clone()).unwrap_err();
assert_eq!(
res,
"Action #0: Smart contract panicked: ERR_QUIT_WRONG_ACC"
);
let roles = dao_roles();
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![]),
("has_3", vec![&user3.account_id]),
("has_23", vec![&user3.account_id]),
("has_234", vec![&user3.account_id, &user4.account_id])
]
);

// user3 quits incorrectly, passing the wrong dao name
let wrong_dao_name = format!("wrong_{}", &dao_name);
let res = quit(&user3, &user3, wrong_dao_name).unwrap_err();
assert_eq!(
res,
"Action #0: Smart contract panicked: ERR_QUIT_WRONG_DAO"
);
let roles = dao_roles();
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![]),
("has_3", vec![&user3.account_id]),
("has_23", vec![&user3.account_id]),
("has_234", vec![&user3.account_id, &user4.account_id])
]
);

// user3 quits
let res = quit(&user3, &user3, dao_name.clone()).unwrap();
assert!(res);
let roles = dao_roles();
assert_eq!(
dao_roles_ref(&roles),
vec![
("all", vec![]),
("council", vec![&root.account_id]),
("has_nobody", vec![]),
("has_2", vec![]),
("has_3", vec![]),
("has_23", vec![]),
("has_234", vec![&user4.account_id])
]
);
}
Loading