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

Astra: add option to act_proposal to not execute the proposal #5

Merged
merged 16 commits into from
Sep 21, 2023
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ export ASTRA_ID=genesis.$CONTRACT_ID
near view $ASTRA_ID get_policy
```

- Verify that the name, purpose, metadata, and council are all configured correctly. Also note the following default values:
- Verify that the name, purpose, metadata, and council are all configured correctly. Also note the following default values for roles and permissions:

```json
{
"roles": [
{
"name": "all",
"kind": "Everyone",
"permissions": ["*:AddProposal"],
"permissions": ["*:AddProposal", "*:Execute"],
"vote_policy": {}
},
{
Expand Down Expand Up @@ -237,6 +237,7 @@ near view $ASTRA_ID get_policy
- `VoteRemove` - _Votes to remove given proposal or bounty (this may be because the proposal is spam or otherwise invalid)._
- `Finalize` - _Finalizes proposal which is cancelled when proposal has expired (this action also returns funds)._
- `MoveToHub` - _Moves a proposal to the hub (this is used to move a proposal into another DAO)._
- `Execute` - Execute a proposal if it was not executed in final vote.
- `VetoProposal` - Veto a proposal. Veto is instant, it will `reject` the proposal and return bond.
- `Dissolve` - Dissolve this DAO: remove all members of the DAO, and sending the remaining funds back to the trust.

Expand Down
6 changes: 3 additions & 3 deletions astra/src/bounties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ mod tests {
},
});
assert_eq!(contract.get_last_bounty_id(), id);
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None, Some(false));
id
}

Expand Down Expand Up @@ -269,7 +269,7 @@ mod tests {
"bounty_done"
);

contract.act_proposal(1, Action::VoteApprove, None);
contract.act_proposal(1, Action::VoteApprove, None, Some(false));
testing_env!(
context.build(),
near_sdk::VMConfig::test(),
Expand All @@ -284,7 +284,7 @@ mod tests {

contract.bounty_claim(0, U64::from(500));
contract.bounty_done(0, None, "Bounty is done 2".to_string());
contract.act_proposal(2, Action::VoteApprove, None);
contract.act_proposal(2, Action::VoteApprove, None, None);
testing_env!(
context.build(),
near_sdk::VMConfig::test(),
Expand Down
69 changes: 57 additions & 12 deletions astra/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,19 @@ mod tests {
}
let id = create_proposal(&mut context, &mut contract);
(context.build(), contract, id)
}
}

fn setup_for_proposals() -> (VMContext, Contract, u64) {
let mut context = VMContextBuilder::new();
testing_env!(context.predecessor_account_id(accounts(1)).build());
let mut contract = Contract::new(
Config::test_config(),
VersionedPolicy::Default(vec![accounts(1)]),
ndc_trust()
);
let id = create_proposal(&mut context, &mut contract);
return (context.build(), contract, id)
}

#[test]
fn test_basics() {
Expand All @@ -359,7 +371,7 @@ mod tests {
assert_eq!(contract.get_proposals(0, 10).len(), 1);

let id = create_proposal(&mut context, &mut contract);
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None, None);
assert_eq!(
contract.get_proposal(id).proposal.status,
ProposalStatus::Approved
Expand All @@ -370,7 +382,7 @@ mod tests {
testing_env!(context
.block_timestamp(1_000_000_000 * 24 * 60 * 60 * 8)
.build());
contract.act_proposal(id, Action::Finalize, None);
contract.act_proposal(id, Action::Finalize, None, None);
assert_eq!(
contract.get_proposal(id).proposal.status,
ProposalStatus::Expired
Expand Down Expand Up @@ -402,7 +414,7 @@ mod tests {
);
let id = create_proposal(&mut context, &mut contract);
assert_eq!(contract.get_proposal(id).proposal.description, "test");
contract.act_proposal(id, Action::RemoveProposal, None);
contract.act_proposal(id, Action::RemoveProposal, None, None);
}

#[test]
Expand All @@ -416,7 +428,7 @@ mod tests {
let mut contract = Contract::new(Config::test_config(), policy, accounts(1));
let id = create_proposal(&mut context, &mut contract);
assert_eq!(contract.get_proposal(id).proposal.description, "test");
contract.act_proposal(id, Action::RemoveProposal, None);
contract.act_proposal(id, Action::RemoveProposal, None, None);
assert_eq!(contract.get_proposals(0, 10).len(), 0);
}

Expand All @@ -433,7 +445,7 @@ mod tests {
testing_env!(context
.block_timestamp(1_000_000_000 * 24 * 60 * 60 * 8)
.build());
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None, None);
}

#[test]
Expand All @@ -447,8 +459,8 @@ mod tests {
ndc_trust()
);
let id = create_proposal(&mut context, &mut contract);
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None, None);
contract.act_proposal(id, Action::VoteApprove, None, None);
}

#[test]
Expand All @@ -468,12 +480,45 @@ mod tests {
role: "missing".to_string(),
},
});
contract.act_proposal(id, Action::VoteApprove, None);
contract.act_proposal(id, Action::VoteApprove, None, None);
let x = contract.get_policy();
// still 2 roles: all and council.
assert_eq!(x.roles.len(), 2);
}

#[test]
fn test_proposal_execution() {
let (_, mut contract, id) = setup_for_proposals();

contract.act_proposal(id, Action::VoteApprove, None, Some(true));
// 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);
assert_eq!(
contract.get_proposal(id).proposal.status,
ProposalStatus::Executed
);
}

#[test]
#[should_panic(expected = "ERR_PROPOSAL_ALREADY_EXECUTED")]
fn test_proposal_double_execution() {
let (_, mut contract, id) = setup_for_proposals();
contract.act_proposal(id, Action::VoteApprove, None, Some(false));
// verify proposal was approved and executed
assert_eq!(
contract.get_proposal(id).proposal.status,
ProposalStatus::Executed
);
amityadav0 marked this conversation as resolved.
Show resolved Hide resolved

// panics if we try to execute again
contract.act_proposal(id, Action::Execute, None, None);
}

#[test]
#[should_panic(expected = "ERR_INVALID_POLICY")]
fn test_fails_adding_invalid_policy() {
Expand Down Expand Up @@ -561,12 +606,12 @@ mod tests {
// Other members vote
context.predecessor_account_id = council(2);
testing_env!(context.clone());
contract.act_proposal(id, Action::VoteApprove, Some("vote on prosposal".to_string()));
contract.act_proposal(id, Action::VoteApprove, Some("vote on prosposal".to_string()), None);
assert!(contract.get_proposal(id).proposal.votes.contains_key(&council(2)));

context.predecessor_account_id = council(3);
testing_env!(context.clone());
contract.act_proposal(id, Action::VoteReject, Some("vote on prosposal".to_string()));
contract.act_proposal(id, Action::VoteReject, Some("vote on prosposal".to_string()), None);
assert!(contract.get_proposal(id).proposal.votes.contains_key(&council(3)));

// Voting body vetos
Expand All @@ -577,7 +622,7 @@ mod tests {
// no more members should be able to vote
context.predecessor_account_id = council(4);
testing_env!(context);
contract.act_proposal(id, Action::VoteApprove, Some("vote on prosposal".to_string()));
contract.act_proposal(id, Action::VoteApprove, Some("vote on prosposal".to_string()), None);
}


Expand Down
4 changes: 2 additions & 2 deletions astra/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ pub fn default_policy(council: Vec<AccountId>) -> Policy {
RolePermission {
name: "all".to_string(),
kind: RoleKind::Everyone,
permissions: vec!["*:AddProposal".to_string()].into_iter().collect(),
permissions: vec!["*:AddProposal".to_string(), "*:Execute".to_string()].into_iter().collect(),
vote_policy: HashMap::default(),
},
RolePermission {
Expand Down Expand Up @@ -400,7 +400,7 @@ impl Policy {
assert!(
matches!(
proposal.status,
ProposalStatus::InProgress | ProposalStatus::Failed
ProposalStatus::InProgress | ProposalStatus::Failed | ProposalStatus::Approved
),
"ERR_PROPOSAL_NOT_IN_PROGRESS"
);
Expand Down
22 changes: 19 additions & 3 deletions astra/src/proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use near_contract_standards::fungible_token::core::ext_ft_core;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::{log, AccountId, Balance, Gas, PromiseOrValue};
use near_sdk::{log, AccountId, Balance, Gas, PromiseOrValue, require};

use crate::policy::UserInfo;
use crate::types::{
Expand Down Expand Up @@ -31,6 +31,7 @@ pub enum ProposalStatus {
Moved,
/// If proposal has failed when finalizing. Allowed to re-finalize again to either expire or approved.
Failed,
Executed,
}

/// Function call arguments.
Expand Down Expand Up @@ -540,10 +541,11 @@ 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>) {
pub fn act_proposal(&mut self, id: u64, action: Action, memo: Option<String>, skip_execution: Option<bool>) {
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();
// Check permissions for the given action.
Expand All @@ -570,11 +572,13 @@ impl Contract {
&policy,
self.get_user_weight(&sender_id),
);

// 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 {
if proposal.status == ProposalStatus::Approved && execute {
self.internal_execute_proposal(&policy, &proposal, id);
proposal.status = ProposalStatus::Executed;
true
} else if proposal.status == ProposalStatus::Removed {
self.internal_reject_proposal(&policy, &proposal, false);
Expand Down Expand Up @@ -604,6 +608,7 @@ impl Contract {
match proposal.status {
ProposalStatus::Approved => {
self.internal_execute_proposal(&policy, &proposal, id);
proposal.status = ProposalStatus::Executed;
}
ProposalStatus::Expired => {
self.internal_reject_proposal(&policy, &proposal, true);
Expand All @@ -615,8 +620,19 @@ impl Contract {
true
}
Action::MoveToHub => false,
Action::Execute => {
require!(proposal.status != ProposalStatus::Executed, "ERR_PROPOSAL_ALREADY_EXECUTED");
// recompute status to check if the proposal is not expired.
proposal.status = policy.proposal_status(&proposal, roles, self.total_delegation_amount);
amityadav0 marked this conversation as resolved.
Show resolved Hide resolved
require!(proposal.status == ProposalStatus::Approved, "ERR_PROPOSAL_NOT_APPROVED");

self.internal_execute_proposal(&policy, &proposal, id);
proposal.status = ProposalStatus::Executed;
true
},
Action::VetoProposal => panic!("Operation not allowed"),
Action::Dissolve => panic!("Operation not allowed"),

};
if update {
self.proposals
Expand Down
2 changes: 2 additions & 0 deletions astra/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum Action {
Finalize,
/// Move a proposal to the hub to shift into another DAO.
MoveToHub,
/// Execute proposal and update proposal status
Execute,
/// Veto hook
VetoProposal,
/// Dissovle hook
Expand Down
8 changes: 4 additions & 4 deletions astra/tests/test_general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ async fn proposal_tests() -> anyhow::Result<()> {
let prop: Proposal = dao.call("get_proposal").args_json(json!({"id": 1})).view().await?.json()?;
assert_eq!(
prop.status,
ProposalStatus::Approved
ProposalStatus::Executed
);

Ok(())
Expand Down Expand Up @@ -372,22 +372,22 @@ async fn test_create_dao_and_use_token() -> anyhow::Result<()> {
// Voting by user who is not a member should fail.
let res = user2.clone()
.call(dao.id(), "act_proposal")
.args_json(json!({"id": 0, "action": Action::VoteApprove}))
.args_json(json!({"id": 0, "action": Action::VoteApprove, "skip_execution": false}))
.max_gas()
.transact()
.await?;
assert!(res.is_failure(), "{:?}", res);
let res = root.clone()
.call(dao.id(), "act_proposal")
.args_json(json!({"id": 0, "action": Action::VoteApprove}))
.args_json(json!({"id": 0, "action": Action::VoteApprove, "skip_execution": false}))
.max_gas()
.transact()
.await?;
assert!(res.is_success(), "{:?}", res);
// Voting second time on the same proposal should fail.
let res = root.clone()
.call(dao.id(), "act_proposal")
.args_json(json!({"id": 0, "action": Action::VoteApprove}))
.args_json(json!({"id": 0, "action": Action::VoteApprove, "skip_execution": false}))
.max_gas()
.transact()
.await?;
Expand Down
2 changes: 1 addition & 1 deletion astra/tests/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub async fn vote(users: Vec<Account>, dao: &Contract, proposal_id: u64) -> anyh
for user in users.into_iter() {
let res = user
.call(dao.id(), "act_proposal")
.args_json(json!({"id": proposal_id, "action": Action::VoteApprove}))
.args_json(json!({"id": proposal_id, "action": Action::VoteApprove, "skip_execution": false}))
.max_gas()
.transact()
.await?;
Expand Down
Loading