diff --git a/sputnikdao2/src/basic_action.rs b/sputnikdao2/src/basic_action.rs new file mode 100644 index 000000000..930d23124 --- /dev/null +++ b/sputnikdao2/src/basic_action.rs @@ -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); + self.policy + .set(&crate::VersionedPolicy::Current(new_policy)); + removed + } +} diff --git a/sputnikdao2/src/lib.rs b/sputnikdao2/src/lib.rs index 695ff6935..cf164811b 100644 --- a/sputnikdao2/src/lib.rs +++ b/sputnikdao2/src/lib.rs @@ -9,11 +9,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; mod bounties; mod delegation; mod policy; diff --git a/sputnikdao2/src/policy.rs b/sputnikdao2/src/policy.rs index 84454afdd..2f90c233f 100644 --- a/sputnikdao2/src/policy.rs +++ b/sputnikdao2/src/policy.rs @@ -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. - /// : - pub permissions: HashSet, + /// Set of proposal actions (on certain kinds of proposals) that this + /// role allow it's members to execute. + /// : + pub permissions: HashSet, /// For each proposal kind, defines voting policy. pub vote_policy: HashMap, } +/// Set of proposal actions (on certain kinds of proposals) that a +/// role allow it's members to execute. +/// : +pub type ProposalPermission = String; + pub struct UserInfo { pub account_id: AccountId, pub amount: Balance, @@ -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> { + /// 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> { let mut roles = HashMap::default(); for role in self.roles.iter() { if role.kind.match_user(&user) { @@ -290,16 +309,14 @@ impl Policy { ) -> (Vec, 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 { @@ -353,6 +370,8 @@ impl Policy { return ProposalStatus::Expired; }; for role in roles { + let role_str = format!("{:#?}", &role); + env::log_str(&role_str); let role_info = self.internal_get_role(&role).expect("ERR_MISSING_ROLE"); let vote_policy = role_info .vote_policy @@ -362,16 +381,23 @@ impl Policy { vote_policy.quorum.0, match &vote_policy.weight_kind { WeightKind::TokenWeight => vote_policy.threshold.to_weight(total_supply), - WeightKind::RoleWeight => vote_policy.threshold.to_weight( - role_info - .kind - .get_role_size() - .expect("ERR_UNSUPPORTED_ROLE") as Balance, - ), + WeightKind::RoleWeight => { + vote_policy + .threshold + .to_weight(match role_info.kind.get_role_size() { + Some(size) => size as Balance, + // skip this role, as it's not a sizable + // one + None => continue, + }) + } }, ); // Check if there is anything voted above the threshold specified by policy for given role. - let vote_counts = proposal.vote_counts.get(&role).expect("ERR_MISSING_ROLE"); + let vote_counts = proposal + .vote_counts + .get(&role) + .unwrap_or_else(|| &[0, 0, 0]); if vote_counts[Vote::Approve as usize] >= threshold { return ProposalStatus::Approved; } else if vote_counts[Vote::Reject as usize] >= threshold { diff --git a/sputnikdao2/src/proposals.rs b/sputnikdao2/src/proposals.rs index d4ef976ef..6db514b1b 100644 --- a/sputnikdao2/src/proposals.rs +++ b/sputnikdao2/src/proposals.rs @@ -432,12 +432,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) { - 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 { @@ -462,19 +468,24 @@ impl Contract { // Updates proposal status with new votes using the policy. proposal.status = policy.proposal_status(&proposal, roles, self.total_delegation_amount); - if proposal.status == ProposalStatus::Approved { - self.internal_execute_proposal(&policy, &proposal); - true - } else if proposal.status == ProposalStatus::Removed { - self.internal_reject_proposal(&policy, &proposal, false); - self.proposals.remove(&id); - false - } else if proposal.status == ProposalStatus::Rejected { - self.internal_reject_proposal(&policy, &proposal, true); - true - } else { - // Still in progress or expired. - true + match proposal.status { + ProposalStatus::Approved => { + self.internal_execute_proposal(&policy, &proposal); + true + } + ProposalStatus::Rejected => { + self.internal_reject_proposal(&policy, &proposal, true); + true + } + ProposalStatus::Removed => { + self.internal_reject_proposal(&policy, &proposal, false); + self.proposals.remove(&id); + false + } + _ => { + // Still in progress, moved or expired. + true + } } } Action::Finalize => { @@ -483,13 +494,31 @@ impl Contract { policy.roles.iter().map(|r| r.name.clone()).collect(), self.total_delegation_amount, ); - assert_eq!( - proposal.status, - ProposalStatus::Expired, - "ERR_PROPOSAL_NOT_EXPIRED" - ); - self.internal_reject_proposal(&policy, &proposal, true); - true + match proposal.status { + // no decision made + ProposalStatus::InProgress => false, + ProposalStatus::Approved => { + self.internal_execute_proposal(&policy, &proposal); + true + } + ProposalStatus::Rejected => { + self.internal_reject_proposal(&policy, &proposal, true); + true + } + ProposalStatus::Removed => { + self.internal_reject_proposal(&policy, &proposal, false); + self.proposals.remove(&id); + false + } + ProposalStatus::Expired => { + self.internal_reject_proposal(&policy, &proposal, true); + true + } + ProposalStatus::Moved => { + // not yet implemented + env::panic_str("ERR_TODO_MOVED_PROPOSAL") + } + } } Action::MoveToHub => false, }; diff --git a/sputnikdao2/src/types.rs b/sputnikdao2/src/types.rs index 02aa54db9..f8b7ec8fc 100644 --- a/sputnikdao2/src/types.rs +++ b/sputnikdao2/src/types.rs @@ -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 { diff --git a/sputnikdao2/tests/test_general.rs b/sputnikdao2/tests/test_general.rs index 76a64900f..6d086bb54 100644 --- a/sputnikdao2/tests/test_general.rs +++ b/sputnikdao2/tests/test_general.rs @@ -84,9 +84,8 @@ fn test_create_dao_and_use_token() { let staking = setup_staking(&root); assert!(view!(dao.get_staking_contract()) - .unwrap_json::() - .as_str() - .is_empty()); + .unwrap_json::>() + .is_none()); add_member_proposal(&root, &dao, user2.account_id.clone()).assert_success(); assert_eq!(view!(dao.get_last_proposal_id()).unwrap_json::(), 1); // Voting by user who is not member should fail. @@ -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::() - .as_str() - .is_empty()); + .unwrap_json::>() + .is_none()); assert_eq!( view!(dao.get_proposal(2)).unwrap_json::().status, ProposalStatus::Approved diff --git a/sputnikdao2/tests/test_quit.rs b/sputnikdao2/tests/test_quit.rs new file mode 100644 index 000000000..f84566648 --- /dev/null +++ b/sputnikdao2/tests/test_quit.rs @@ -0,0 +1,416 @@ +#![allow(clippy::ref_in_deref)] +#![allow(clippy::identity_op)] + +use crate::utils::{ + add_member_to_role_proposal, add_proposal, setup_dao, should_fail_with, vote, Contract, +}; +use near_sdk::AccountId; +use near_sdk_sim::{call, to_yocto, view}; +use near_sdk_sim::{ExecutionResult, UserAccount}; +use sputnikdao2::{ + Action, Policy, Proposal, ProposalInput, ProposalKind, ProposalPermission, ProposalStatus, + RoleKind, RolePermission, VersionedPolicy, +}; +use std::collections::HashMap; +use std::collections::HashSet; + +mod utils; + +const KILO: u128 = 1000; +const MEGA: u128 = KILO * KILO; +const YOTTA: u128 = MEGA * MEGA * MEGA * MEGA; + +fn user(id: u32) -> AccountId { + format!("user{}", id).parse().unwrap() +} + +fn new_role(name: String, permissions: HashSet) -> RolePermission { + RolePermission { + name, + kind: RoleKind::Group(HashSet::new()), + permissions, + vote_policy: HashMap::new(), + } +} + +/// Updates the policy, pushing the roles into it. +fn policy_extend_roles(root: &UserAccount, dao: &Contract, roles: Vec) { + { + let mut policy = view!(dao.get_policy()).unwrap_json::(); + policy.roles.extend(roles); + 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::(); + assert_eq!(change_policy, 1); + call!( + root, + dao.act_proposal(change_policy - 1, Action::VoteApprove, None) + ) + .assert_success(); + }; +} + +fn add_user_to_roles( + root: &UserAccount, + dao: &Contract, + user: &UserAccount, + role_names: Vec<&str>, +) { + for role_name in role_names { + add_member_to_role_proposal(root, dao, user.account_id.clone(), role_name.to_string()) + .assert_success(); + + // approval + let proposal = view!(dao.get_last_proposal_id()).unwrap_json::(); + call!( + root, + dao.act_proposal(proposal - 1, Action::VoteApprove, None) + ) + .assert_success(); + } +} + +/// Given a RolePermission, get it's members in a sorted `Vec`. +fn role_members(role_permission: &sputnikdao2::RolePermission) -> Vec { + if let RoleKind::Group(ref members) = role_permission.kind { + let mut members = members.iter().cloned().collect::>(); + members.sort(); + members + } else { + vec![] + } +} + +type RoleNamesAndMembers = Vec<(String, Vec)>; + +/// Get dao role names and their members +fn dao_roles(dao: &Contract) -> RoleNamesAndMembers { + view!(dao.get_policy()) + .unwrap_json::() + .roles + .into_iter() + .map(|role_permission| (role_permission.name.clone(), role_members(&role_permission))) + .collect() +} + +type RoleNamesAndMembersRef<'a> = Vec<(&'a str, Vec<&'a AccountId>)>; +/// Makes references into a `RoleNamesAndMembers` +/// so they are easier to compare against. +#[allow(clippy::ptr_arg)] +fn dao_roles_ref(dao_roles: &RoleNamesAndMembers) -> RoleNamesAndMembersRef { + dao_roles + .iter() + .map(|(name, members)| (name.as_str(), members.iter().collect())) + .collect::)>>() +} + +/// Quit from the dao. +fn quit( + dao: &Contract, + user: &UserAccount, + user_check: &UserAccount, + dao_name_check: String, +) -> Result { + 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_check), + deposit = to_yocto("0") + ); + match res.status() { + ExecutionStatus::SuccessValue(_bytes) => Ok(res.unwrap_json::()), + ExecutionStatus::Failure(err) => Err(err.to_string()), + _ => panic!("unexpected status"), + } +} + +/// Adds some dummy proposal, for the votes to be tested on. +/// (transfers of 1 yocto-near to `receiver`). +fn add_transfer_proposal(root: &UserAccount, dao: &Contract, receiver: &UserAccount) -> u64 { + let proposal_input = ProposalInput { + description: "new policy".to_string(), + kind: ProposalKind::Transfer { + token_id: None, + receiver_id: receiver.account_id(), + amount: 1u128.into(), + msg: None, + }, + }; + call!(root, dao.add_proposal(proposal_input), deposit = 1 * YOTTA).unwrap_json::() +} + +/// Issue #41 "Quitting the DAO" tests +#[test] +fn test_quitting_the_dao() { + 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 role_none = new_role("has_nobody".to_string(), HashSet::new()); + let role_2 = new_role("has_2".to_string(), HashSet::new()); + let role_3 = new_role("has_3".to_string(), HashSet::new()); + let role_23 = new_role("has_23".to_string(), HashSet::new()); + let role_234 = new_role("has_234".to_string(), HashSet::new()); + + policy_extend_roles( + &root, + &dao, + vec![role_none, role_2, role_3, role_23, role_234], + ); + + add_user_to_roles(&root, &dao, &user2, vec!["has_2", "has_23", "has_234"]); + add_user_to_roles(&root, &dao, &user3, vec!["has_3", "has_23", "has_234"]); + add_user_to_roles(&root, &dao, &user4, vec!["has_234"]); + + // initial check, + // when nobody has quit yet + let roles = dao_roles(&dao); + 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::(); + let dao_name = &config.name; + + // ok: user2 quits + let res = quit(&dao, &user2, &user2, dao_name.clone()).unwrap(); + assert!(res); + let roles = dao_roles(&dao); + 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]) + ] + ); + + // ok: user2 quits again + // (makes no change) + let res = quit(&dao, &user2, &user2, dao_name.clone()).unwrap(); + assert!(!res); + let roles = dao_roles(&dao); + 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]) + ] + ); + + // fail: user3 quits passing the wrong user name + let res = quit(&dao, &user3, &user2, dao_name.clone()).unwrap_err(); + assert_eq!( + res, + "Action #0: Smart contract panicked: ERR_QUIT_WRONG_ACC" + ); + let roles = dao_roles(&dao); + 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]) + ] + ); + + // fail: user3 quits passing the wrong dao name + let wrong_dao_name = format!("wrong_{}", &dao_name); + let res = quit(&dao, &user3, &user3, wrong_dao_name).unwrap_err(); + assert_eq!( + res, + "Action #0: Smart contract panicked: ERR_QUIT_WRONG_DAO" + ); + let roles = dao_roles(&dao); + 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]) + ] + ); + + // ok: user3 quits + let res = quit(&dao, &user3, &user3, dao_name.clone()).unwrap(); + assert!(res); + let roles = dao_roles(&dao); + 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]) + ] + ); +} + +/// Tests a role with Ratio = 1/2 with two members, +/// when one member votes and then the other one quits. +/// There should be a way for the users to "finalize" +/// the decision on the proposal, since it would now only +/// require that single vote. +/// +/// https://github.com/near-daos/sputnik-dao-contract/issues/41#issuecomment-970170648 +#[test] +fn test_quit_removes_votes1() { + 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")); + + // users (2, 3) will share a role, + // and only user2 will vote in approval, then user3 quits. + // then assert that the proposals can get approved from only 1 vote. + + let dao_name = { + let config = view!(dao.get_config()).unwrap_json::(); + config.name + }; + + { + let permissions = vec!["*:*".to_string()].into_iter().collect(); + let role_23 = new_role("has_23".to_string(), permissions); + policy_extend_roles(&root, &dao, vec![role_23]); + } + + add_user_to_roles(&root, &dao, &user2, vec!["has_23"]); + add_user_to_roles(&root, &dao, &user3, vec!["has_23"]); + + // adds two transfer proposals + let t1 = add_transfer_proposal(&root, &dao, &user4); + let t2 = add_transfer_proposal(&root, &dao, &user4); + + // user2 votes in approval of both + vote(vec![&user2], &dao, t1); + vote(vec![&user2], &dao, t2); + + // user3 quits role + let res = quit(&dao, &user3, &user3, dao_name).unwrap(); + assert!(res); + + // ok: user2 finalizes t1 + let user4amount = user4.account().unwrap().amount; + call!(user2, dao.act_proposal(t1, Action::Finalize, None)).assert_success(); + assert_eq!( + view!(dao.get_proposal(t1)).unwrap_json::().status, + ProposalStatus::Approved + ); + // confirm user4 received the transfer + assert_eq!( + user4amount + // the bounty + + 1, + user4.account().unwrap().amount + ); + + // fail: user3 tries to finelize t2 + let res = call!(user3, dao.act_proposal(t2, Action::Finalize, None)); + should_fail_with(res, 0, "ERR_PERMISSION_DENIED"); +} + +/// Tests a role with Ratio = 1/2 with two members, +/// when one member votes and then quits. +/// That single vote should not cause (nor allow) a state change +/// in the proposal. That vote should be removed instead. +/// +/// https://github.com/near-daos/sputnik-dao-contract/issues/41#issuecomment-971474598 +#[test] +fn test_quit_removes_votes2() { + 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")); + + // users (2, 3) will share a role, + // and only user2 will vote in approval and then quit. + // then assert that the proposals cannot get approved from only 1 vote. + + let dao_name = { + let config = view!(dao.get_config()).unwrap_json::(); + config.name + }; + + { + let permissions = vec!["*:*".to_string()].into_iter().collect(); + let role_23 = new_role("has_23".to_string(), permissions); + policy_extend_roles(&root, &dao, vec![role_23]); + } + + add_user_to_roles(&root, &dao, &user2, vec!["has_23"]); + add_user_to_roles(&root, &dao, &user3, vec!["has_23"]); + + // adds two transfer proposals + let t1 = add_transfer_proposal(&root, &dao, &user4); + let t2 = add_transfer_proposal(&root, &dao, &user4); + + // user2 votes in approval of both + vote(vec![&user2], &dao, t1); + vote(vec![&user2], &dao, t2); + + // user2 quits role + let res = quit(&dao, &user2, &user2, dao_name).unwrap(); + assert!(res); + + // user2 tries to finalize t1 + let res = call!(user2, dao.act_proposal(t1, Action::Finalize, None)); + should_fail_with(res, 0, "ERR_FINALIZE_(TODO)"); + // confirm t1 did not get approved + assert_eq!( + view!(dao.get_proposal(t1)).unwrap_json::().status, + ProposalStatus::InProgress + ); + + // user3 tries to finalize t2 + let res = call!(user3, dao.act_proposal(t2, Action::Finalize, None)); + should_fail_with(res, 0, "ERR_FINALIZE_(TODO)"); + // confirm t2 did not get approved + assert_eq!( + view!(dao.get_proposal(t2)).unwrap_json::().status, + ProposalStatus::InProgress + ); +} diff --git a/sputnikdao2/tests/utils/mod.rs b/sputnikdao2/tests/utils/mod.rs index 0cc750ece..4866b8369 100644 --- a/sputnikdao2/tests/utils/mod.rs +++ b/sputnikdao2/tests/utils/mod.rs @@ -21,7 +21,7 @@ near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { STAKING_WASM_BYTES => "../sputnik-staking/res/sputnik_staking.wasm", } -type Contract = ContractAccount; +pub type Contract = ContractAccount; pub fn base_token() -> Option { None @@ -34,6 +34,22 @@ pub fn should_fail(r: ExecutionResult) { } } +pub fn should_fail_with(r: ExecutionResult, action: u32, err: &str) { + let err = format!("Action #{}: Smart contract panicked: {}", action, err); + match r.status() { + ExecutionStatus::Failure(txerr_) => { + assert_eq!(txerr_.to_string(), err) + } + ExecutionStatus::Unknown => panic!("Got Unknown. Should have failed with {}", err), + ExecutionStatus::SuccessValue(_v) => { + panic!("Got SuccessValue. Should have failed with {}", err) + } + ExecutionStatus::SuccessReceiptId(_id) => { + panic!("Got SuccessReceiptId. Should have failed with {}", err) + } + } +} + pub fn setup_dao() -> (UserAccount, Contract) { let root = init_simulator(None); let config = Config { @@ -86,16 +102,22 @@ pub fn add_member_proposal( root: &UserAccount, dao: &Contract, member_id: AccountId, +) -> ExecutionResult { + add_member_to_role_proposal(root, dao, member_id, "council".to_string()) +} + +pub fn add_member_to_role_proposal( + root: &UserAccount, + dao: &Contract, + member_id: AccountId, + role: String, ) -> ExecutionResult { add_proposal( root, dao, ProposalInput { description: "test".to_string(), - kind: ProposalKind::AddMemberToRole { - member_id: member_id, - role: "council".to_string(), - }, + kind: ProposalKind::AddMemberToRole { member_id, role }, }, ) }