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

Add self-destruct functionality to the DAO #63

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
de24536
add max_days_of_inactivity parameter inside the config
constantindogaru Nov 23, 2021
7303515
create a separate structure for the self destruct config
constantindogaru Nov 23, 2021
edbc796
add block of code for checking the inactivity of a dao
constantindogaru Nov 23, 2021
60176ed
correct some bits
constantindogaru Nov 23, 2021
437b5e3
add documentation
constantindogaru Nov 23, 2021
a5c8c97
add method to check activity for all daos
constantindogaru Nov 24, 2021
58a98e9
fix some bugs
constantindogaru Nov 24, 2021
c4f3225
Merge branch 'main' of https://github.com/near-daos/sputnik-dao-contr…
constantindogaru Nov 25, 2021
a51a93e
add unit tests
constantindogaru Nov 25, 2021
7b5ef97
add method to initialize a sputnik factory
constantindogaru Nov 25, 2021
7ca7e1f
add simulation test
constantindogaru Nov 25, 2021
6bf2260
complete simulation test
constantindogaru Nov 25, 2021
b50d89a
add binary files
constantindogaru Nov 25, 2021
4bde839
fix error related to storage
constantindogaru Nov 26, 2021
e66863c
add another tbd
constantindogaru Nov 26, 2021
6435454
make sure locked_amount is always up to date
constantindogaru Nov 26, 2021
e081473
Merge branch 'fix_locked_amount' of https://github.com/ctindogaru/spu…
constantindogaru Nov 26, 2021
5d20f99
verify that amunt was actually refunded to the dedicated account
constantindogaru Nov 26, 2021
e580134
Merge branch 'main' of https://github.com/near-daos/sputnik-dao-contr…
constantindogaru Dec 8, 2021
2a2ee88
push binaries
constantindogaru Dec 8, 2021
d29c061
add explanations on TBD
constantindogaru Dec 8, 2021
a66a1b2
avoid confusion around refund_amount
constantindogaru Dec 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified sputnik-staking/res/sputnik_staking.wasm
Binary file not shown.
Binary file modified sputnikdao-factory/res/sputnikdao_factory.wasm
Binary file not shown.
Binary file modified sputnikdao-factory2/res/sputnikdao_factory2.wasm
Binary file not shown.
46 changes: 46 additions & 0 deletions sputnikdao-factory2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::UnorderedSet;
use near_sdk::json_types::{Base64VecU8, U128};
use near_sdk::PromiseResult;
use near_sdk::{assert_self, env, ext_contract, near_bindgen, AccountId, Gas, Promise, PublicKey};

const CODE: &[u8] = include_bytes!("../../sputnikdao2/res/sputnikdao2.wasm");
Expand All @@ -19,6 +20,12 @@ pub trait ExtSelf {
attached_deposit: U128,
predecessor_account_id: AccountId,
) -> bool;
fn on_check_daos_activity(&mut self, account_id: AccountId);
}

#[ext_contract(ext_dao)]
pub trait ExtDao {
fn is_active(&mut self) -> bool;
}

#[near_bindgen]
Expand Down Expand Up @@ -60,6 +67,45 @@ impl SputnikDAOFactory {
.collect()
}

// Check daos activity.
pub fn check_daos_activity(&self) {
for dao in self.daos.iter() {
ext_dao::is_active(dao.clone(), 0, Gas(10_000_000_000_000)).then(
ext_self::on_check_daos_activity(
dao.clone(),
env::current_account_id(),
0,
Gas(10_000_000_000_000),
),
);
}
}

pub fn on_check_daos_activity(&mut self, account_id: AccountId) {
assert_eq!(
env::promise_results_count(),
1,
"DAO Factory Error: This is a callback method"
);

match env::promise_result(0) {
PromiseResult::NotReady => panic!(
"DAO Factory Error: Received PromiseResult::NotReady for {:?}",
account_id
),
PromiseResult::Failed => panic!(
"DAO Factory Error: Received PromiseResult::Failed for {:?}",
account_id
),
PromiseResult::Successful(result) => {
let is_active = near_sdk::serde_json::from_slice::<bool>(&result).unwrap();
if !is_active {
self.daos.remove(&account_id);
}
}
}
}

#[payable]
pub fn create(
&mut self,
Expand Down
Binary file modified sputnikdao/res/sputnikdao.wasm
Binary file not shown.
3 changes: 2 additions & 1 deletion sputnikdao2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ version = "1.4.0"
[dev-dependencies]
near-sdk-sim = "4.0.0-pre.4"
test-token = { path = "../test-token" }
sputnik-staking = { path = "../sputnik-staking" }
sputnik-staking = { path = "../sputnik-staking" }
sputnikdao-factory2 = { path = "../sputnikdao-factory2" }
Binary file modified sputnikdao2/res/sputnikdao2.wasm
Binary file not shown.
102 changes: 102 additions & 0 deletions sputnikdao2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,44 @@ impl Contract {
let storage_cost = ((blob_len + 32) as u128) * env::storage_byte_cost();
Promise::new(account_id).transfer(storage_cost)
}

/// Check if this DAO is still active (active = it has a recently created proposal).
/// If active, do nothing.
/// If inactive, transfer the available amount of NEAR in this DAO to the dedicated account.
pub fn is_active(&mut self) -> bool {
let config = self.get_config().self_destruct_config;
if config.is_none() {
return true; // self-destruct is not configured, so this DAO is always considered active
}
let config = config.unwrap();

if self.last_proposal_id == 0 {
return true; // nothing to check since there are no proposals in this DAO
}

let last_proposal: Proposal = self
.proposals
.get(&(self.last_proposal_id - 1))
.expect("ERR_NO_PROPOSAL")
.into();

let allowed_inactivity_time =
1_000_000_000 * 60 * 60 * 24 * config.max_days_of_inactivity.0;

let mut active = false;
if last_proposal.submission_time.0 + allowed_inactivity_time > env::block_timestamp() {
active = true; // it means this DAO is still active
}

if !active {
Promise::new(config.dedicated_account).transfer(self.get_available_amount().0);

// TBD: Do we need to delete this DAO? If so, we should delete this contract and also send
// self.get_locked_storage_amount().0 to the dedicated account.
}

active
}
}

// Cross-contract calls
Expand Down Expand Up @@ -207,11 +245,13 @@ pub extern "C" fn store_blob() {

#[cfg(test)]
mod tests {
use near_sdk::json_types::U64;
use near_sdk::test_utils::{accounts, VMContextBuilder};
use near_sdk::testing_env;
use near_sdk_sim::to_yocto;

use crate::proposals::ProposalStatus;
use crate::types::SelfDestructConfig;

use super::*;

Expand Down Expand Up @@ -272,6 +312,68 @@ mod tests {
});
}

#[test]
fn test_is_active_no_config() {
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).into()]),
);
create_proposal(&mut context, &mut contract);
assert!(contract.get_config().self_destruct_config.is_none());
assert!(contract.is_active());
}

#[test]
fn test_is_active_no_proposal() {
let mut context = VMContextBuilder::new();
testing_env!(context.predecessor_account_id(accounts(1)).build());
let mut config = Config::test_config();
config.self_destruct_config = Some(SelfDestructConfig {
max_days_of_inactivity: U64(7),
dedicated_account: accounts(2),
});
let mut contract =
Contract::new(config, VersionedPolicy::Default(vec![accounts(1).into()]));
assert!(contract.get_config().self_destruct_config.is_some());
assert!(contract.is_active());
}

#[test]
fn test_is_active_success() {
let mut context = VMContextBuilder::new();
testing_env!(context.predecessor_account_id(accounts(1)).build());
let mut config = Config::test_config();
config.self_destruct_config = Some(SelfDestructConfig {
max_days_of_inactivity: U64(7),
dedicated_account: accounts(2),
});
let mut contract =
Contract::new(config, VersionedPolicy::Default(vec![accounts(1).into()]));
create_proposal(&mut context, &mut contract);

assert!(contract.get_config().self_destruct_config.is_some());
assert!(contract.is_active());
}

#[test]
fn test_is_active_failure() {
let mut context = VMContextBuilder::new();
testing_env!(context.predecessor_account_id(accounts(1)).build());
let mut config = Config::test_config();
config.self_destruct_config = Some(SelfDestructConfig {
max_days_of_inactivity: U64(0),
dedicated_account: accounts(2),
});
let mut contract =
Contract::new(config, VersionedPolicy::Default(vec![accounts(1).into()]));
create_proposal(&mut context, &mut contract);

assert!(contract.get_config().self_destruct_config.is_some());
assert!(!contract.is_active());
}

#[test]
#[should_panic(expected = "ERR_PERMISSION_DENIED")]
fn test_remove_proposal_denied() {
Expand Down
18 changes: 17 additions & 1 deletion sputnikdao2/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::Base64VecU8;
use near_sdk::json_types::{Base64VecU8, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, Gas};

Expand All @@ -14,6 +14,17 @@ pub const GAS_FOR_UPGRADE_SELF_DEPLOY: Gas = Gas(30_000_000_000_000);

pub const GAS_FOR_UPGRADE_REMOTE_DEPLOY: Gas = Gas(10_000_000_000_000);

/// Configuration of the DAO for self-destruction.
#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct SelfDestructConfig {
/// Remove this DAO if no proposal was passed in the last `max_days_of_inactivity`.
pub max_days_of_inactivity: U64,
/// Dedicated account to receive the funds locked in this DAO if `max_days_of_inactivity`
/// is reached and this DAO has to be removed.
pub dedicated_account: AccountId,
}

/// Configuration of the DAO.
#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
Expand All @@ -25,6 +36,10 @@ pub struct Config {
/// Generic metadata. Can be used by specific UI to store additional data.
/// This is not used by anything in the contract.
pub metadata: Base64VecU8,
/// self-destruct configuration of the DAO.
/// The self-destruction is triggered only if the DAO becomes inactive.
/// If not specified, DAO is allowed to be inactive for an indefinite period of time.
pub self_destruct_config: Option<SelfDestructConfig>,
}

#[cfg(test)]
Expand All @@ -34,6 +49,7 @@ impl Config {
name: "Test".to_string(),
purpose: "to test".to_string(),
metadata: Base64VecU8(vec![]),
self_destruct_config: None,
}
}
}
Expand Down
75 changes: 75 additions & 0 deletions sputnikdao2/tests/test_general.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;

use near_sdk::json_types::U128;
use near_sdk::serde_json::json;
use near_sdk::AccountId;
use near_sdk_sim::{call, to_yocto, view};

Expand Down Expand Up @@ -225,6 +226,80 @@ fn test_create_dao_and_use_token() {
);
}

#[test]
fn test_check_daos_activity() {
let (root, dao_factory) = setup_dao_factory();

call!(
&root,
dao_factory.create(
"dao1".parse().unwrap(),
None,
"{\"config\": {\"name\": \"Test1\", \"purpose\": \"to test1\", \"metadata\": \"\"}, \"policy\": [\"dao1\"]}".as_bytes().to_vec().into()
),
deposit = to_yocto("10")
).assert_success();

call!(
&root,
dao_factory.create(
"dao2".parse().unwrap(),
None,
"{\"config\": {\"name\": \"Test2\", \"purpose\": \"to test2\", \"metadata\": \"\", \"self_destruct_config\": {\"max_days_of_inactivity\": \"0\", \"dedicated_account\": \"user1\"}}, \"policy\": [\"dao2\"]}".as_bytes().to_vec().into()
),
deposit = to_yocto("10")
).assert_success();

call!(
&root,
dao_factory.create(
"dao3".parse().unwrap(),
None,
"{\"config\": {\"name\": \"Test3\", \"purpose\": \"to test3\", \"metadata\": \"\", \"self_destruct_config\": {\"max_days_of_inactivity\": \"7\", \"dedicated_account\": \"root\"}}, \"policy\": [\"dao3\"]}".as_bytes().to_vec().into()
),
deposit = to_yocto("10")
).assert_success();

let dao_list = view!(dao_factory.get_dao_list()).unwrap_json::<Vec<AccountId>>();
assert_eq!(3, dao_list.len());
assert_eq!("dao1.dao_factory", dao_list[0].as_str());
assert_eq!("dao2.dao_factory", dao_list[1].as_str());
assert_eq!("dao3.dao_factory", dao_list[2].as_str());

let user1 = root.create_user(user(1), to_yocto("1000"));
let proposal_id: u64 = root
.call(
"dao2.dao_factory".parse().unwrap(),
"add_proposal",
&json!({
"proposal": ProposalInput {
description: "test".to_string(),
kind: ProposalKind::AddMemberToRole {
member_id: user1.account_id(),
role: "dao2".to_string(),
},
},
})
.to_string()
.into_bytes(),
300_000_000_000_000,
to_yocto("1"),
)
.unwrap_json();
assert_eq!(0, proposal_id);

let before_refund = user1.account().unwrap().amount;
assert_eq!(before_refund, to_yocto("1000"));
call!(&root, dao_factory.check_daos_activity()).assert_success();
let after_refund = user1.account().unwrap().amount;
assert!(before_refund < after_refund);

let dao_list = view!(dao_factory.get_dao_list()).unwrap_json::<Vec<AccountId>>();
assert_eq!(2, dao_list.len());
assert_eq!("dao1.dao_factory", dao_list[0].as_str());
assert_eq!("dao3.dao_factory", dao_list[1].as_str());
}

/// Test various cases that must fail.
#[test]
fn test_failures() {
Expand Down
16 changes: 16 additions & 0 deletions sputnikdao2/tests/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ use sputnik_staking::ContractContract as StakingContract;
use sputnikdao2::{
Action, Config, ContractContract as DAOContract, ProposalInput, ProposalKind, VersionedPolicy,
};
use sputnikdao_factory2::SputnikDAOFactoryContract;
use test_token::ContractContract as TestTokenContract;

near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
DAO_WASM_BYTES => "res/sputnikdao2.wasm",
DAO_FACTORY_WASM_BYTES => "../sputnikdao-factory2/res/sputnikdao_factory2.wasm",
TEST_TOKEN_WASM_BYTES => "../test-token/res/test_token.wasm",
STAKING_WASM_BYTES => "../sputnik-staking/res/sputnik_staking.wasm",
}
Expand All @@ -32,12 +34,26 @@ pub fn should_fail(r: ExecutionResult) {
}
}

pub fn setup_dao_factory() -> (UserAccount, ContractAccount<SputnikDAOFactoryContract>) {
let root = init_simulator(None);
let dao_factory = deploy!(
contract: SputnikDAOFactoryContract,
contract_id: "dao_factory".to_string(),
bytes: &DAO_FACTORY_WASM_BYTES,
signer_account: root,
deposit: to_yocto("200"),
init_method: new()
);
(root, dao_factory)
}

pub fn setup_dao() -> (UserAccount, Contract) {
let root = init_simulator(None);
let config = Config {
name: "test".to_string(),
purpose: "to test".to_string(),
metadata: Base64VecU8(vec![]),
self_destruct_config: None,
};
let dao = deploy!(
contract: DAOContract,
Expand Down
Binary file modified test-token/res/test_token.wasm
Binary file not shown.