diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e332421..bd114d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,16 +20,28 @@ Change log entries are to be added to the Unreleased section. Example entry: ## Unreleased ++ Update repo to the latest near-sdk and related deps ++ Integrate hooks ++ Add option to act_proposal to not execute the proposal ++ Add events to veto and dissolve hooks ++ Integrate cooldown + ### Features New methods: -- A.. +- `veto_hook`: Vetos any proposal.(must be called by authority with permission) +- `dissolve_hook`: Dissolves the DAO by removing all members, closing all active proposals and returning bonds. Extended types: +- ProposalStatus: `Executed` +- Action: `Veto` and `Dissolve` + New types: +- `ContractStatus`: Active or Dissolved + ### Breaking Changes - B... diff --git a/README.md b/README.md index 9db27735..b9bc41f2 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ near view $ASTRA_ID get_policy }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } diff --git a/astra/TestPlan.md b/astra/TestPlan.md index ab916a7d..3c5a7a97 100644 --- a/astra/TestPlan.md +++ b/astra/TestPlan.md @@ -135,6 +135,7 @@ Confirm the default policy acts as it should. }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } @@ -194,6 +195,7 @@ Each }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } @@ -253,6 +255,7 @@ Each }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } @@ -312,6 +315,7 @@ Each }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } @@ -371,6 +375,7 @@ Each }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } @@ -446,6 +451,7 @@ Each group council can have different threshold criteria for consensus. Confirm }, "proposal_bond": "1000000000000000000000000", "proposal_period": "604800000000000", + "cooldown": "0", "bounty_bond": "1000000000000000000000000", "bounty_forgiveness_period": "86400000000000" } diff --git a/astra/src/lib.rs b/astra/src/lib.rs index 23a00262..c8bd24ca 100644 --- a/astra/src/lib.rs +++ b/astra/src/lib.rs @@ -265,7 +265,7 @@ mod tests { use near_sdk::{testing_env, VMContext}; use near_units::parse_near; - use crate::proposals::ProposalStatus; + use crate::proposals::{ProposalStatus, PolicyParameters}; use crate::test_utils::*; use super::*; @@ -322,6 +322,7 @@ mod tests { default_vote_policy: VotePolicy::default(), proposal_bond: U128(10u128.pow(24)), proposal_period: U64::from(1_000_000_000 * 60 * 60 * 24 * 7), + cooldown: U64::from(0), bounty_bond: U128(10u128.pow(24)), bounty_forgiveness_period: U64::from(1_000_000_000 * 60 * 60 * 24), } @@ -354,7 +355,7 @@ mod tests { ndc_trust() ); let id = create_proposal(&mut context, &mut contract); - return (context.build(), contract, id) + (context.build(), contract, id) } #[test] @@ -519,6 +520,28 @@ mod tests { contract.act_proposal(id, Action::Execute, None, None); } + #[test] + #[should_panic(expected = "ERR_PROPOSAL_STILL_ACTIVE")] + fn test_cooldown() { + let (_, mut contract, id) = setup_for_proposals(); + let mut policy = contract.policy.get().unwrap().to_policy(); + policy.update_parameters(&PolicyParameters{ + cooldown: Some(U64::from(1_000 * 60 * 60)), proposal_bond: None, + proposal_period: None, bounty_bond: None, + bounty_forgiveness_period: None + }); + contract.policy.set(&VersionedPolicy::Current(policy)); + + contract.act_proposal(id, Action::VoteApprove, None, None); + // verify proposal wasn't executed during final vote + assert_eq!( + contract.get_proposal(id).proposal.status, + ProposalStatus::Approved + ); + + contract.act_proposal(id, Action::Execute, None, None); + } + #[test] #[should_panic(expected = "ERR_INVALID_POLICY")] fn test_fails_adding_invalid_policy() { @@ -635,7 +658,7 @@ mod tests { assert!(!res.roles.is_empty()); context.predecessor_account_id = acc_voting_body(); - testing_env!(context.clone()); + testing_env!(context); contract.dissolve_hook(); res = contract.policy.get().unwrap().to_policy(); assert!(res.roles.is_empty()); diff --git a/astra/src/policy.rs b/astra/src/policy.rs index b4bed5b3..cb7446da 100644 --- a/astra/src/policy.rs +++ b/astra/src/policy.rs @@ -154,6 +154,8 @@ pub struct Policy { pub proposal_bond: U128, /// Expiration period for proposals. pub proposal_period: U64, + /// The execution of the proposal can only occur once the cooldown period has elapsed (measured in milliseconds). + pub cooldown: U64, /// Bond for claiming a bounty. pub bounty_bond: U128, /// Period in which giving up on bounty is not punished. @@ -204,6 +206,7 @@ pub fn default_policy(council: Vec) -> Policy { default_vote_policy: VotePolicy::default(), proposal_bond: U128(10u128.pow(24)), proposal_period: U64::from(1_000_000_000 * 60 * 60 * 24 * 7), + cooldown: U64::from(0), bounty_bond: U128(10u128.pow(24)), bounty_forgiveness_period: U64::from(1_000_000_000 * 60 * 60 * 24), } @@ -268,17 +271,20 @@ impl Policy { } pub fn update_parameters(&mut self, parameters: &PolicyParameters) { - if parameters.proposal_bond.is_some() { - self.proposal_bond = parameters.proposal_bond.unwrap(); + if let Some(proposal_bond) = parameters.proposal_bond { + self.proposal_bond = proposal_bond; } - if parameters.proposal_period.is_some() { - self.proposal_period = parameters.proposal_period.unwrap(); + if let Some(proposal_period) = parameters.proposal_period { + self.proposal_period = proposal_period; } - if parameters.bounty_bond.is_some() { - self.bounty_bond = parameters.bounty_bond.unwrap(); + if let Some(cooldown) = parameters.cooldown { + self.cooldown = cooldown; } - if parameters.bounty_forgiveness_period.is_some() { - self.bounty_forgiveness_period = parameters.bounty_forgiveness_period.unwrap(); + if let Some(bounty_bond) = parameters.bounty_bond { + self.bounty_bond = bounty_bond; + } + if let Some(bounty_forgiveness_period) = parameters.bounty_forgiveness_period { + self.bounty_forgiveness_period = bounty_forgiveness_period; } env::log_str("Successfully updated the policy parameters."); } @@ -444,6 +450,14 @@ impl Policy { } proposal.status.clone() } + + /// Returns true if cooldown is over else false + pub fn is_past_cooldown(&mut self, submission_time: U64) -> bool { + if env::block_timestamp() >= (self.cooldown.0 * 1_000_000) + submission_time.0 { + return true; + } + false + } } #[cfg(test)] @@ -616,6 +630,7 @@ mod tests { proposal_bond: Some(U128(10u128.pow(26))), proposal_period: None, bounty_bond: None, + cooldown: None, bounty_forgiveness_period: Some(U64::from(1_000_000_000 * 60 * 60 * 24 * 5)), }; policy.update_parameters(&new_parameters); diff --git a/astra/src/proposals.rs b/astra/src/proposals.rs index 79ba9df6..9e03aedc 100644 --- a/astra/src/proposals.rs +++ b/astra/src/proposals.rs @@ -52,6 +52,7 @@ pub struct ActionCall { pub struct PolicyParameters { pub proposal_bond: Option, pub proposal_period: Option, + pub cooldown: Option, pub bounty_bond: Option, pub bounty_forgiveness_period: Option, } @@ -545,9 +546,10 @@ impl Contract { if self.status == ContractStatus::Dissolved { panic!("Cannot perform this action, dao is dissolved!") } - let execute = !skip_execution.unwrap_or(false); let mut proposal: Proposal = self.proposals.get(&id).expect("ERR_NO_PROPOSAL").into(); - let policy = self.policy.get().unwrap().to_policy(); + let mut policy = self.policy.get().unwrap().to_policy(); + + let execute = !skip_execution.unwrap_or(false) && policy.is_past_cooldown(proposal.submission_time); // Check permissions for the given action. let (roles, allowed) = policy.can_execute_action(self.internal_user_info(), &proposal.kind, &action); @@ -600,6 +602,7 @@ impl Contract { // - if the number of votes in the group has changed (new members has been added) - // the proposal can loose it's approved state. In this case new proposal needs to be made, this one can only expire. Action::Finalize => { + require!(policy.is_past_cooldown(proposal.submission_time), "ERR_PROPOSAL_STILL_ACTIVE"); proposal.status = policy.proposal_status( &proposal, policy.roles.iter().map(|r| r.name.clone()).collect(), @@ -622,6 +625,7 @@ impl Contract { Action::MoveToHub => false, Action::Execute => { require!(proposal.status != ProposalStatus::Executed, "ERR_PROPOSAL_ALREADY_EXECUTED"); + require!(policy.is_past_cooldown(proposal.submission_time), "ERR_PROPOSAL_STILL_ACTIVE"); // recompute status to check if the proposal is not expired. proposal.status = policy.proposal_status(&proposal, roles, self.total_delegation_amount); require!(proposal.status == ProposalStatus::Approved, "ERR_PROPOSAL_NOT_APPROVED"); diff --git a/astra/tests/test_general.rs b/astra/tests/test_general.rs index d385dd3b..8ca41a35 100644 --- a/astra/tests/test_general.rs +++ b/astra/tests/test_general.rs @@ -106,7 +106,7 @@ async fn test_multi_council() -> anyhow::Result<()> { RolePermission { name: "all".to_string(), kind: RoleKind::Everyone, - permissions: vec!["*:AddProposal".to_string()].into_iter().collect(), + permissions: vec!["*:Execute".to_string(), "*:AddProposal".to_string()].into_iter().collect(), vote_policy: HashMap::default(), }, RolePermission { @@ -125,6 +125,7 @@ async fn test_multi_council() -> anyhow::Result<()> { default_vote_policy: VotePolicy::default(), proposal_bond: U128(10u128.pow(24)), proposal_period: U64::from(1_000_000_000 * 60 * 60 * 24 * 7), + cooldown: U64::from(0), bounty_bond: U128(10u128.pow(24)), bounty_forgiveness_period: U64::from(1_000_000_000 * 60 * 60 * 24), };