diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bb36ab10..c781ec805 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,8 +88,8 @@ jobs: - name: Install binaryen run: | set -e - curl -L https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz | tar xzf - - echo "`pwd`/binaryen-version_105/bin" >> $GITHUB_PATH + curl -L https://github.com/WebAssembly/binaryen/releases/download/version_119/binaryen-version_119-x86_64-linux.tar.gz | tar xzf - + echo "`pwd`/binaryen-version_119/bin" >> $GITHUB_PATH # triggers all build.rs steps - name: Trigger build.rs steps run: | diff --git a/Cargo.lock b/Cargo.lock index 84f1cc7dc..eca201b69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1555,22 +1555,13 @@ dependencies = [ "serde", ] -[[package]] -name = "near-contract-standards" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7421d0a5c7aeb57b37a0cf3a71a7aefbbdc7727f9811d9dd86fa55e4f0de4d3" -dependencies = [ - "near-sdk 3.1.0", -] - [[package]] name = "near-contract-standards" version = "4.0.0-pre.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c4f636adbffbd9399610cb6445894c64c6c8fcf9ea4e607021f252a1e0459f" dependencies = [ - "near-sdk 4.0.0-pre.7", + "near-sdk", "serde", "serde_json", ] @@ -1841,23 +1832,6 @@ dependencies = [ "regex", ] -[[package]] -name = "near-sdk" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7383e242d3e07bf0951e8589d6eebd7f18bb1c1fc5fbec3fad796041a6aebd1" -dependencies = [ - "base64 0.13.0", - "borsh 0.8.2", - "bs58", - "near-primitives-core 0.4.0", - "near-sdk-macros 3.1.0", - "near-vm-logic 4.0.0-pre.1", - "serde", - "serde_json", - "wee_alloc", -] - [[package]] name = "near-sdk" version = "4.0.0-pre.7" @@ -1868,7 +1842,7 @@ dependencies = [ "borsh 0.9.3", "bs58", "near-primitives-core 0.10.0", - "near-sdk-macros 4.0.0-pre.7", + "near-sdk-macros", "near-sys", "near-vm-logic 0.10.0", "once_cell", @@ -1877,30 +1851,6 @@ dependencies = [ "wee_alloc", ] -[[package]] -name = "near-sdk-core" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284a78d9eb8eda58330462fa0023a6d7014c941df1f0387095e7dfd1dc0f2bce" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "near-sdk-macros" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2037337438f97d1ce5f7c896cf229dc56dacd5c01142d1ef95a7d778cde6ce7d" -dependencies = [ - "near-sdk-core", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "near-sdk-macros" version = "4.0.0-pre.7" @@ -1913,23 +1863,6 @@ dependencies = [ "syn", ] -[[package]] -name = "near-sdk-sim" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f9be539455733f5cff63febaee07dcdc48c0a9940856257a1989e84a5552d7a" -dependencies = [ - "funty", - "lazy-static-include", - "near-crypto 0.1.0", - "near-pool", - "near-primitives 0.1.0-pre.1", - "near-runtime", - "near-sdk 3.1.0", - "near-store", - "near-vm-logic 4.0.0-pre.1", -] - [[package]] name = "near-sdk-sim" version = "4.0.0-pre.7" @@ -1942,7 +1875,7 @@ dependencies = [ "near-pool", "near-primitives 0.1.0-pre.1", "near-runtime", - "near-sdk 4.0.0-pre.7", + "near-sdk", "near-store", "near-vm-logic 4.0.0-pre.1", ] @@ -2867,9 +2800,9 @@ name = "sputnik-staking" version = "1.0.0" dependencies = [ "hex", - "near-contract-standards 4.0.0-pre.7", - "near-sdk 4.0.0-pre.7", - "near-sdk-sim 4.0.0-pre.7", + "near-contract-standards", + "near-sdk", + "near-sdk-sim", "test-token", ] @@ -2877,8 +2810,8 @@ dependencies = [ name = "sputnikdao-factory2" version = "0.2.1" dependencies = [ - "near-sdk 4.0.0-pre.7", - "near-sdk-sim 4.0.0-pre.7", + "near-sdk", + "near-sdk-sim", ] [[package]] @@ -2886,27 +2819,15 @@ name = "sputnikdao2" version = "2.3.1" dependencies = [ "hex", - "near-contract-standards 4.0.0-pre.7", - "near-sdk 4.0.0-pre.7", - "near-sdk-sim 4.0.0-pre.7", + "near-contract-standards", + "near-sdk", + "near-sdk-sim", "serde_with", "sputnik-staking", "sputnikdao-factory2", "test-token", ] -[[package]] -name = "sputnikdao2-gasfix" -version = "2.0.0" -dependencies = [ - "hex", - "near-contract-standards 3.2.0", - "near-sdk 3.1.0", - "near-sdk-sim 3.2.0", - "sputnik-staking", - "test-token", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3017,8 +2938,8 @@ dependencies = [ name = "test-token" version = "0.1.0" dependencies = [ - "near-contract-standards 4.0.0-pre.7", - "near-sdk 4.0.0-pre.7", + "near-contract-standards", + "near-sdk", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 862588015..bb50970fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "sputnik-staking", "sputnikdao2", - "sputnikdao2-gasfix", "sputnikdao-factory2", "test-token" ] diff --git a/build.sh b/build.sh index bac7506eb..72ebe95bc 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,5 @@ set -e cargo +stable build --target wasm32-unknown-unknown --release cp target/wasm32-unknown-unknown/release/sputnik_staking.wasm ./sputnik-staking/res/ cp target/wasm32-unknown-unknown/release/sputnikdao2.wasm ./sputnikdao2/res/ -cp target/wasm32-unknown-unknown/release/sputnikdao2_gasfix.wasm ./sputnikdao2-gasfix/res/ cp target/wasm32-unknown-unknown/release/sputnikdao_factory2.wasm ./sputnikdao-factory2/res/ cp target/wasm32-unknown-unknown/release/test_token.wasm ./test-token/res/ \ No newline at end of file diff --git a/sputnikdao-factory2/build_docker.sh b/sputnikdao-factory2/build_docker.sh index 243f92a55..fa645c311 100755 --- a/sputnikdao-factory2/build_docker.sh +++ b/sputnikdao-factory2/build_docker.sh @@ -14,7 +14,7 @@ docker create \ --mount type=bind,source=$DIR/..,target=/host \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --name=$NAME \ - -w /host/sputnikdao_factory2 \ + -w /host/sputnikdao-factory2 \ -e RUSTFLAGS='-C link-arg=-s' \ -it \ nearprotocol/contract-builder \ diff --git a/sputnikdao-factory2/res/sputnikdao_factory2.wasm b/sputnikdao-factory2/res/sputnikdao_factory2.wasm index 29dcacab4..8fcc3908e 100755 Binary files a/sputnikdao-factory2/res/sputnikdao_factory2.wasm and b/sputnikdao-factory2/res/sputnikdao_factory2.wasm differ diff --git a/sputnikdao2-gasfix/Cargo.toml b/sputnikdao2-gasfix/Cargo.toml deleted file mode 100644 index d46cc9558..000000000 --- a/sputnikdao2-gasfix/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "sputnikdao2-gasfix" -version = "2.0.0" -authors = ["Sputnik Devs "] -edition = "2018" -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[profile.release] -codegen-units = 1 -# Tell `rustc` to optimize for small code size. -opt-level = "z" -lto = true -debug = false -panic = "abort" -overflow-checks = true - -[dependencies] -near-sdk = "3.1.0" -near-contract-standards = "3.1.0" -hex = "0.4.2" - -[dev-dependencies] -near-sdk-sim = "3.1.0" -test-token = { path = "../test-token" } -sputnik-staking = { path = "../sputnik-staking" } \ No newline at end of file diff --git a/sputnikdao2-gasfix/README.md b/sputnikdao2-gasfix/README.md deleted file mode 100644 index c5bb6af3b..000000000 --- a/sputnikdao2-gasfix/README.md +++ /dev/null @@ -1 +0,0 @@ -# Sputnik DAO v2 with GAS FIXES for v1.26.0 diff --git a/sputnikdao2-gasfix/build.sh b/sputnikdao2-gasfix/build.sh deleted file mode 100755 index 2dc4b8d1c..000000000 --- a/sputnikdao2-gasfix/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e - -RUSTFLAGS='-C link-arg=-s' cargo +stable build --target wasm32-unknown-unknown --release -cp target/wasm32-unknown-unknown/release/sputnikdao2_gasfix.wasm ./res/ diff --git a/sputnikdao2-gasfix/build_docker.sh b/sputnikdao2-gasfix/build_docker.sh deleted file mode 100755 index afa0d9b28..000000000 --- a/sputnikdao2-gasfix/build_docker.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -# Exit script as soon as a command fails. -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -NAME="build_sputnik_gasfix" - -if docker ps -a --format '{{.Names}}' | grep -Eq "^${NAME}\$"; then - echo "Container exists" -else -docker create \ - --mount type=bind,source=$DIR/..,target=/host \ - --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ - --name=$NAME \ - -w /host/sputnikdao2 \ - -e RUSTFLAGS='-C link-arg=-s' \ - -it \ - nearprotocol/contract-builder \ - /bin/bash -fi - -docker start $NAME -docker exec -it $NAME /bin/bash -c "rustup toolchain install 1.56.0; rustup default 1.56.0; rustup target add wasm32-unknown-unknown; cargo build --target wasm32-unknown-unknown --release" - -mkdir -p res -cp $DIR/../target/wasm32-unknown-unknown/release/sputnikdao2_gasfix.wasm $DIR/res/ - diff --git a/sputnikdao2-gasfix/res/sputnikdao2_gasfix.wasm b/sputnikdao2-gasfix/res/sputnikdao2_gasfix.wasm deleted file mode 100755 index 6bfc9cffc..000000000 Binary files a/sputnikdao2-gasfix/res/sputnikdao2_gasfix.wasm and /dev/null differ diff --git a/sputnikdao2-gasfix/src/bounties.rs b/sputnikdao2-gasfix/src/bounties.rs deleted file mode 100644 index f56b3f8d5..000000000 --- a/sputnikdao2-gasfix/src/bounties.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::convert::TryFrom; - -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{WrappedDuration, WrappedTimestamp, U128}; -use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{env, near_bindgen, AccountId, Promise, PromiseOrValue}; - -use crate::*; - -/// Information recorded about claim of the bounty by given user. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct BountyClaim { - /// Bounty id that was claimed. - bounty_id: u64, - /// Start time of the claim. - start_time: WrappedTimestamp, - /// Deadline specified by claimer. - deadline: WrappedDuration, - /// Completed? - completed: bool, -} - -/// Bounty information. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] -#[serde(crate = "near_sdk::serde")] -pub struct Bounty { - /// Description of the bounty. - pub description: String, - /// Token the bounty will be paid out. - pub token: AccountId, - /// Amount to be paid out. - pub amount: U128, - /// How many times this bounty can be done. - pub times: u32, - /// Max deadline from claim that can be spend on this bounty. - pub max_deadline: WrappedDuration, -} - -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Clone, Debug))] -#[serde(crate = "near_sdk::serde")] -pub enum VersionedBounty { - Default(Bounty), -} - -impl From for Bounty { - fn from(v: VersionedBounty) -> Self { - match v { - VersionedBounty::Default(b) => b, - } - } -} - -impl Contract { - /// Adds bounty to the storage and returns it's id. - /// Must not fail. - pub(crate) fn internal_add_bounty(&mut self, bounty: &Bounty) -> u64 { - let id = self.last_bounty_id; - self.bounties - .insert(&id, &VersionedBounty::Default(bounty.clone())); - self.last_bounty_id += 1; - id - } - - /// This must be called when proposal to payout bounty has been voted either successfully or not. - pub(crate) fn internal_execute_bounty_payout( - &mut self, - id: u64, - receiver_id: &AccountId, - success: bool, - ) -> PromiseOrValue<()> { - let mut bounty: Bounty = self.bounties.get(&id).expect("ERR_NO_BOUNTY").into(); - let (claims, claim_idx) = self.internal_get_claims(id, &receiver_id); - self.internal_remove_claim(id, claims, claim_idx); - if success { - let res = self.internal_payout( - &bounty.token, - receiver_id, - bounty.amount.0, - format!("Bounty {} payout", id), - None, - ); - if bounty.times == 0 { - self.bounties.remove(&id); - } else { - bounty.times -= 1; - self.bounties.insert(&id, &VersionedBounty::Default(bounty)); - } - res - } else { - PromiseOrValue::Value(()) - } - } - - fn internal_find_claim(&self, bounty_id: u64, claims: &[BountyClaim]) -> Option { - for i in 0..claims.len() { - if claims[i].bounty_id == bounty_id { - return Some(i); - } - } - None - } -} - -#[near_bindgen] -impl Contract { - /// Claim given bounty by caller with given expected duration to execute. - /// Bond must be attached to the claim. - /// Fails if already claimed `times` times. - #[payable] - pub fn bounty_claim(&mut self, id: u64, deadline: WrappedDuration) { - let bounty: Bounty = self.bounties.get(&id).expect("ERR_NO_BOUNTY").into(); - let policy = self.policy.get().unwrap().to_policy(); - assert_eq!( - env::attached_deposit(), - policy.bounty_bond.0, - "ERR_BOUNTY_WRONG_BOND" - ); - let claims_count = self.bounty_claims_count.get(&id).unwrap_or_default(); - assert!(claims_count < bounty.times, "ERR_BOUNTY_ALL_CLAIMED"); - assert!( - deadline.0 <= bounty.max_deadline.0, - "ERR_BOUNTY_WRONG_DEADLINE" - ); - self.bounty_claims_count.insert(&id, &(claims_count + 1)); - let mut claims = self - .bounty_claimers - .get(&env::predecessor_account_id()) - .unwrap_or_default(); - claims.push(BountyClaim { - bounty_id: id, - start_time: WrappedTimestamp::from(env::block_timestamp()), - deadline, - completed: false, - }); - self.bounty_claimers - .insert(&env::predecessor_account_id(), &claims); - } - - /// Removes given claims from this bounty and user's claims. - fn internal_remove_claim(&mut self, id: u64, mut claims: Vec, claim_idx: usize) { - claims.remove(claim_idx); - if claims.len() == 0 { - self.bounty_claimers.remove(&env::predecessor_account_id()); - } else { - self.bounty_claimers - .insert(&env::predecessor_account_id(), &claims); - } - let count = self.bounty_claims_count.get(&id).unwrap() - 1; - self.bounty_claims_count.insert(&id, &count); - } - - fn internal_get_claims(&mut self, id: u64, sender_id: &AccountId) -> (Vec, usize) { - let claims = self - .bounty_claimers - .get(&sender_id) - .expect("ERR_NO_BOUNTY_CLAIMS"); - let claim_idx = self - .internal_find_claim(id, &claims) - .expect("ERR_NO_BOUNTY_CLAIM"); - (claims, claim_idx) - } - - /// Report that bounty is done. Creates a proposal to vote for paying out the bounty. - /// Only creator of the claim can call `done` on bounty that is still in progress. - /// On expired, anyone can call it to free up the claim slot. - pub fn bounty_done(&mut self, id: u64, account_id: Option, description: String) { - let sender_id = account_id.unwrap_or_else(|| env::predecessor_account_id()); - let (mut claims, claim_idx) = self.internal_get_claims(id, &sender_id); - assert!(!claims[claim_idx].completed, "ERR_BOUNTY_CLAIM_COMPLETED"); - if env::block_timestamp() > claims[claim_idx].start_time.0 + claims[claim_idx].deadline.0 { - // Expired. Nothing to do. - self.internal_remove_claim(id, claims, claim_idx); - } else { - // Still under deadline. Only the user themself can call this. - assert_eq!( - sender_id, - env::predecessor_account_id(), - "ERR_BOUNTY_DONE_MUST_BE_SELF" - ); - self.add_proposal(ProposalInput { - description, - kind: ProposalKind::BountyDone { - bounty_id: id, - receiver_id: ValidAccountId::try_from(sender_id.clone()).unwrap(), - }, - }); - claims[claim_idx].completed = true; - self.bounty_claimers.insert(&sender_id, &claims); - } - } - - /// Give up working on the bounty. - pub fn bounty_giveup(&mut self, id: u64) -> PromiseOrValue<()> { - let policy = self.policy.get().unwrap().to_policy(); - let (claims, claim_idx) = self.internal_get_claims(id, &env::predecessor_account_id()); - let result = if env::block_timestamp() - claims[claim_idx].start_time.0 - > policy.bounty_forgiveness_period.0 - { - // If user over the forgiveness period. - PromiseOrValue::Value(()) - } else { - // Within forgiveness period. - Promise::new(env::predecessor_account_id()) - .transfer(policy.bounty_bond.0) - .into() - }; - self.internal_remove_claim(id, claims, claim_idx); - result - } -} - -#[cfg(test)] -mod tests { - use near_sdk::test_utils::{accounts, VMContextBuilder}; - use near_sdk::{testing_env, MockedBlockchain}; - use near_sdk_sim::to_yocto; - - use crate::proposals::{ProposalInput, ProposalKind}; - use crate::types::BASE_TOKEN; - use crate::{Action, Config}; - - use super::*; - - fn add_bounty(context: &mut VMContextBuilder, contract: &mut Contract, times: u32) -> u64 { - testing_env!(context.attached_deposit(to_yocto("1")).build()); - let id = contract.add_proposal(ProposalInput { - description: "test".to_string(), - kind: ProposalKind::AddBounty { - bounty: Bounty { - description: "test bounty".to_string(), - token: BASE_TOKEN.to_string(), - amount: U128(to_yocto("10")), - times, - max_deadline: WrappedDuration::from(1_000), - }, - }, - }); - assert_eq!(contract.get_last_bounty_id(), id); - contract.act_proposal(id, Action::VoteApprove, None); - id - } - - /// Adds a bounty, and tests it's full lifecycle. - #[test] - fn test_bounty_lifecycle() { - 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()]), - ); - add_bounty(&mut context, &mut contract, 2); - - assert_eq!(contract.get_last_bounty_id(), 1); - assert_eq!(contract.get_bounty(0).bounty.times, 2); - - contract.bounty_claim(0, WrappedDuration::from(500)); - assert_eq!(contract.get_bounty_claims(accounts(1)).len(), 1); - assert_eq!(contract.get_bounty_number_of_claims(0), 1); - - contract.bounty_giveup(0); - assert_eq!(contract.get_bounty_claims(accounts(1)).len(), 0); - assert_eq!(contract.get_bounty_number_of_claims(0), 0); - - contract.bounty_claim(0, WrappedDuration::from(500)); - assert_eq!(contract.get_bounty_claims(accounts(1)).len(), 1); - assert_eq!(contract.get_bounty_number_of_claims(0), 1); - - contract.bounty_done(0, None, "Bounty is done".to_string()); - assert!(contract.get_bounty_claims(accounts(1))[0].completed); - - assert_eq!(contract.get_last_proposal_id(), 2); - assert_eq!( - contract.get_proposal(1).proposal.kind.to_policy_label(), - "bounty_done" - ); - - contract.act_proposal(1, Action::VoteApprove, None); - - assert_eq!(contract.get_bounty_claims(accounts(1)).len(), 0); - assert_eq!(contract.get_bounty(0).bounty.times, 1); - - contract.bounty_claim(0, WrappedDuration::from(500)); - contract.bounty_done(0, None, "Bounty is done 2".to_string()); - contract.act_proposal(2, Action::VoteApprove, None); - - assert_eq!(contract.get_bounty(0).bounty.times, 0); - } - - #[test] - #[should_panic(expected = "ERR_BOUNTY_ALL_CLAIMED")] - fn test_bounty_claim_not_allowed() { - 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()]), - ); - let id = add_bounty(&mut context, &mut contract, 1); - contract.bounty_claim(id, WrappedDuration::from(500)); - contract.bounty_done(id, None, "Bounty is done 2".to_string()); - contract.bounty_claim(id, WrappedDuration::from(500)); - } -} diff --git a/sputnikdao2-gasfix/src/delegation.rs b/sputnikdao2-gasfix/src/delegation.rs deleted file mode 100644 index 4d3a28f3e..000000000 --- a/sputnikdao2-gasfix/src/delegation.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; - -impl Contract { - pub fn get_user_weight(&self, account_id: &AccountId) -> Balance { - self.delegations.get(account_id).unwrap_or_default() - } -} - -#[near_bindgen] -impl Contract { - #[payable] - pub fn register_delegation(&mut self, account_id: &AccountId) { - let staking_id = self.staking_id.clone().expect("ERR_NO_STAKING"); - assert_eq!( - env::predecessor_account_id(), - staking_id, - "ERR_INVALID_CALLER" - ); - assert_eq!(env::attached_deposit(), 16 * env::storage_byte_cost()); - self.delegations.insert(account_id, &0); - } - - pub fn delegate(&mut self, account_id: &AccountId, amount: U128) { - let staking_id = self.staking_id.clone().expect("ERR_NO_STAKING"); - assert_eq!( - env::predecessor_account_id(), - staking_id, - "ERR_INVALID_CALLER" - ); - let prev_amount = self - .delegations - .get(account_id) - .expect("ERR_NOT_REGISTERED"); - self.delegations - .insert(account_id, &(prev_amount + amount.0)); - self.total_delegation_amount += amount.0; - } - - pub fn undelegate(&mut self, account_id: &AccountId, amount: U128) { - let staking_id = self.staking_id.clone().expect("ERR_NO_STAKING"); - assert_eq!( - env::predecessor_account_id(), - staking_id, - "ERR_INVALID_CALLER" - ); - let prev_amount = self.delegations.get(account_id).unwrap_or_default(); - assert!(prev_amount >= amount.0, "ERR_INVALID_STAKING_CONTRACT"); - self.delegations - .insert(account_id, &(prev_amount - amount.0)); - self.total_delegation_amount -= amount.0; - } -} diff --git a/sputnikdao2-gasfix/src/lib.rs b/sputnikdao2-gasfix/src/lib.rs deleted file mode 100644 index fb004a155..000000000 --- a/sputnikdao2-gasfix/src/lib.rs +++ /dev/null @@ -1,326 +0,0 @@ -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::{LazyOption, LookupMap}; -#[cfg(target_arch = "wasm32")] -use near_sdk::env::BLOCKCHAIN_INTERFACE; -use near_sdk::json_types::{Base58CryptoHash, ValidAccountId, U128}; -use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{ - env, near_bindgen, AccountId, Balance, BorshStorageKey, CryptoHash, PanicOnDefault, Promise, -}; - -use crate::bounties::{Bounty, BountyClaim, VersionedBounty}; -pub use crate::policy::{Policy, RoleKind, RolePermission, VersionedPolicy, VotePolicy}; -use crate::proposals::VersionedProposal; -pub use crate::proposals::{Proposal, ProposalInput, ProposalKind, ProposalStatus}; -pub use crate::types::{Action, Config}; - -mod bounties; -mod delegation; -mod policy; -mod proposals; -mod types; -pub mod views; - -near_sdk::setup_alloc!(); - -#[cfg(target_arch = "wasm32")] -const BLOCKCHAIN_INTERFACE_NOT_SET_ERR: &str = "Blockchain interface not set."; - -#[derive(BorshStorageKey, BorshSerialize)] -pub enum StorageKeys { - Config, - Policy, - Delegations, - Proposals, - Bounties, - BountyClaimers, - BountyClaimCounts, - Blobs, -} - -#[near_bindgen] -#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] -pub struct Contract { - /// DAO configuration. - pub config: LazyOption, - /// Voting and permissions policy. - pub policy: LazyOption, - - /// Amount of $NEAR locked for storage / bonds. - pub locked_amount: Balance, - - /// Vote staking contract id. That contract must have this account as owner. - pub staking_id: Option, - /// Delegated token total amount. - pub total_delegation_amount: Balance, - /// Delegations per user. - pub delegations: LookupMap, - - /// Last available id for the proposals. - pub last_proposal_id: u64, - /// Proposal map from ID to proposal information. - pub proposals: LookupMap, - - /// Last available id for the bounty. - pub last_bounty_id: u64, - /// Bounties map from ID to bounty information. - pub bounties: LookupMap, - /// Bounty claimers map per user. Allows quickly to query for each users their claims. - pub bounty_claimers: LookupMap>, - /// Count of claims per bounty. - pub bounty_claims_count: LookupMap, - - /// Large blob storage. - pub blobs: LookupMap, -} - -#[near_bindgen] -impl Contract { - #[init] - pub fn new(config: Config, policy: VersionedPolicy) -> Self { - Self { - config: LazyOption::new(StorageKeys::Config, Some(&config)), - policy: LazyOption::new(StorageKeys::Policy, Some(&policy.upgrade())), - staking_id: None, - total_delegation_amount: 0, - delegations: LookupMap::new(StorageKeys::Delegations), - last_proposal_id: 0, - proposals: LookupMap::new(StorageKeys::Proposals), - last_bounty_id: 0, - bounties: LookupMap::new(StorageKeys::Bounties), - bounty_claimers: LookupMap::new(StorageKeys::BountyClaimers), - bounty_claims_count: LookupMap::new(StorageKeys::BountyClaimCounts), - blobs: LookupMap::new(StorageKeys::Blobs), - // TODO: only accounts for contract but not for this state object. Can just add fixed size of it. - locked_amount: env::storage_byte_cost() * (env::storage_usage() as u128), - } - } - - /// Should only be called by this contract on migration. - /// This is NOOP implementation. KEEP IT if you haven't changed contract state. - /// If you have changed state, you need to implement migration from old state (keep the old struct with different name to deserialize it first). - /// After migrate goes live on MainNet, return this implementation for next updates. - #[init(ignore_state)] - pub fn migrate() -> Self { - assert_eq!( - env::predecessor_account_id(), - env::current_account_id(), - "ERR_NOT_ALLOWED" - ); - let this: Contract = env::state_read().expect("ERR_CONTRACT_IS_NOT_INITIALIZED"); - this - } - - /// Remove blob from contract storage and pay back to original storer. - /// Only original storer can call this. - pub fn remove_blob(&mut self, hash: Base58CryptoHash) -> Promise { - let hash: CryptoHash = hash.into(); - let account_id = self.blobs.remove(&hash).expect("ERR_NO_BLOB"); - assert_eq!( - env::predecessor_account_id(), - account_id, - "ERR_INVALID_CALLER" - ); - env::storage_remove(&hash); - let blob_len = env::register_len(u64::MAX - 1).unwrap(); - let storage_cost = ((blob_len + 32) as u128) * env::storage_byte_cost(); - self.locked_amount -= storage_cost; - Promise::new(account_id).transfer(storage_cost) - } -} - -/// Stores attached data into blob store and returns hash of it. -/// Implemented to avoid loading the data into WASM for optimal gas usage. -#[cfg(target_arch = "wasm32")] -#[no_mangle] -pub extern "C" fn store_blob() { - env::setup_panic_hook(); - env::set_blockchain_interface(Box::new(near_blockchain::NearBlockchain {})); - let mut contract: Contract = env::state_read().expect("ERR_CONTRACT_IS_NOT_INITIALIZED"); - unsafe { - BLOCKCHAIN_INTERFACE.with(|b| { - // Load input into register 0. - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .input(0); - // Compute sha256 hash of register 0 and store in 1. - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .sha256(u64::MAX as _, 0 as _, 1); - // Check if such blob already stored. - assert_eq!( - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .storage_has_key(u64::MAX as _, 1 as _), - 0, - "ERR_ALREADY_EXISTS" - ); - // Get length of the input argument and check that enough $NEAR has been attached. - let blob_len = b - .borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .register_len(0); - let storage_cost = ((blob_len + 32) as u128) * env::storage_byte_cost(); - assert!( - env::attached_deposit() >= storage_cost, - "ERR_NOT_ENOUGH_DEPOSIT:{}", - storage_cost - ); - contract.locked_amount += storage_cost; - // Store value of register 0 into key = register 1. - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .storage_write(u64::MAX as _, 1 as _, u64::MAX as _, 0 as _, 2); - // Load register 1 into blob_hash and save into LookupMap. - let blob_hash = [0u8; 32]; - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .read_register(1, blob_hash.as_ptr() as _); - contract - .blobs - .insert(&blob_hash, &env::predecessor_account_id()); - // Return from function value of register 1. - let blob_hash_str = near_sdk::serde_json::to_string(&Base58CryptoHash::from(blob_hash)) - .unwrap() - .into_bytes(); - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .value_return(blob_hash_str.len() as _, blob_hash_str.as_ptr() as _); - }); - } - env::state_write(&contract); -} - -#[cfg(test)] -mod tests { - use near_sdk::test_utils::{accounts, VMContextBuilder}; - use near_sdk::{testing_env, MockedBlockchain}; - use near_sdk_sim::to_yocto; - - use crate::proposals::ProposalStatus; - use crate::types::BASE_TOKEN; - - use super::*; - - fn create_proposal(context: &mut VMContextBuilder, contract: &mut Contract) -> u64 { - testing_env!(context.attached_deposit(to_yocto("1")).build()); - contract.add_proposal(ProposalInput { - description: "test".to_string(), - kind: ProposalKind::Transfer { - token_id: BASE_TOKEN.to_string(), - receiver_id: accounts(2).into(), - amount: U128(to_yocto("100")), - msg: None, - }, - }) - } - - #[test] - fn test_basics() { - 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()]), - ); - let id = create_proposal(&mut context, &mut contract); - assert_eq!(contract.get_proposal(id).proposal.description, "test"); - assert_eq!(contract.get_proposals(0, 10).len(), 1); - - let id = create_proposal(&mut context, &mut contract); - contract.act_proposal(id, Action::VoteApprove, None); - assert_eq!( - contract.get_proposal(id).proposal.status, - ProposalStatus::Approved - ); - - let id = create_proposal(&mut context, &mut contract); - // proposal expired, finalize. - testing_env!(context - .block_timestamp(1_000_000_000 * 24 * 60 * 60 * 8) - .build()); - contract.act_proposal(id, Action::Finalize, None); - assert_eq!( - contract.get_proposal(id).proposal.status, - ProposalStatus::Expired - ); - - // non council adding proposal per default policy. - testing_env!(context - .predecessor_account_id(accounts(2)) - .attached_deposit(to_yocto("1")) - .build()); - let _id = contract.add_proposal(ProposalInput { - description: "test".to_string(), - kind: ProposalKind::AddMemberToRole { - member_id: accounts(2).into(), - role: "council".to_string(), - }, - }); - } - - #[test] - #[should_panic(expected = "ERR_PERMISSION_DENIED")] - fn test_remove_proposal_denied() { - 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()]), - ); - let id = create_proposal(&mut context, &mut contract); - assert_eq!(contract.get_proposal(id).proposal.description, "test"); - contract.act_proposal(id, Action::RemoveProposal, None); - } - - #[test] - fn test_remove_proposal_allowed() { - let mut context = VMContextBuilder::new(); - testing_env!(context.predecessor_account_id(accounts(1)).build()); - let mut policy = VersionedPolicy::Default(vec![accounts(1).into()]).upgrade(); - policy.to_policy_mut().roles[1] - .permissions - .insert("*:RemoveProposal".to_string()); - let mut contract = Contract::new(Config::test_config(), policy); - let id = create_proposal(&mut context, &mut contract); - assert_eq!(contract.get_proposal(id).proposal.description, "test"); - contract.act_proposal(id, Action::RemoveProposal, None); - assert_eq!(contract.get_proposals(0, 10).len(), 0); - } - - #[test] - fn test_vote_expired_proposal() { - 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()]), - ); - let id = create_proposal(&mut context, &mut contract); - testing_env!(context - .block_timestamp(1_000_000_000 * 24 * 60 * 60 * 8) - .build()); - contract.act_proposal(id, Action::VoteApprove, None); - } - - #[test] - #[should_panic(expected = "ERR_ALREADY_VOTED")] - fn test_vote_twice() { - 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(), accounts(2).into()]), - ); - let id = create_proposal(&mut context, &mut contract); - contract.act_proposal(id, Action::VoteApprove, None); - contract.act_proposal(id, Action::VoteApprove, None); - } -} diff --git a/sputnikdao2-gasfix/src/policy.rs b/sputnikdao2-gasfix/src/policy.rs deleted file mode 100644 index 3e6c3d6c7..000000000 --- a/sputnikdao2-gasfix/src/policy.rs +++ /dev/null @@ -1,404 +0,0 @@ -use std::cmp::min; -use std::collections::{HashMap, HashSet}; - -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{WrappedDuration, U128}; -use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{env, AccountId, Balance}; - -use crate::proposals::{Proposal, ProposalKind, ProposalStatus, Vote}; -use crate::types::Action; - -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -pub enum RoleKind { - /// Matches everyone, who is not matched by other roles. - Everyone, - /// Member greater or equal than given balance. Can use `1` as non-zero balance. - Member(Balance), - /// Set of accounts. - Group(HashSet), -} - -impl RoleKind { - /// Checks if user matches given role. - pub fn match_user(&self, user: &UserInfo) -> bool { - match self { - RoleKind::Everyone => true, - RoleKind::Member(amount) => user.amount >= *amount, - RoleKind::Group(accounts) => accounts.contains(&user.account_id), - } - } - - /// Returns the number of people in the this role or None if not supported role kind. - pub fn get_role_size(&self) -> Option { - match self { - RoleKind::Group(accounts) => Some(accounts.len()), - _ => None, - } - } - - pub fn add_member_to_group(&mut self, member_id: &AccountId) -> Result<(), ()> { - match self { - RoleKind::Group(accounts) => { - accounts.insert(member_id.clone()); - Ok(()) - } - _ => Err(()), - } - } - - pub fn remove_member_from_group(&mut self, member_id: &AccountId) -> Result<(), ()> { - match self { - RoleKind::Group(accounts) => { - accounts.remove(member_id); - Ok(()) - } - _ => Err(()), - } - } -} - -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -pub struct RolePermission { - /// Name of the role to display to the user. - 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, - /// For each proposal kind, defines voting policy. - pub vote_policy: HashMap, -} - -pub struct UserInfo { - pub account_id: AccountId, - pub amount: Balance, -} - -/// Direct weight or ratio to total weight, used for the voting policy. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -#[serde(untagged)] -pub enum WeightOrRatio { - Weight(U128), - Ratio(u64, u64), -} - -impl WeightOrRatio { - /// Convert weight or ratio to specific weight given total weight. - pub fn to_weight(&self, total_weight: Balance) -> Balance { - match self { - WeightOrRatio::Weight(weight) => min(weight.0, total_weight), - WeightOrRatio::Ratio(num, denom) => min( - (*num as u128 * total_weight) / *denom as u128 + 1, - total_weight, - ), - } - } -} - -/// How the voting policy votes get weigthed. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -pub enum WeightKind { - /// Using token amounts and total delegated at the moment. - TokenWeight, - /// Weight of the group role. Roles that don't have scoped group are not supported. - RoleWeight, -} - -/// Defines configuration of the vote. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -pub struct VotePolicy { - /// Kind of weight to use for votes. - pub weight_kind: WeightKind, - /// Minimum number required for vote to finalize. - /// If weight kind is TokenWeight - this is minimum number of tokens required. - /// This allows to avoid situation where the number of staked tokens from total supply is too small. - /// If RoleWeight - this is minimum umber of votes. - /// This allows to avoid situation where the role is got too small but policy kept at 1/2, for example. - pub quorum: U128, - /// How many votes to pass this vote. - pub threshold: WeightOrRatio, -} - -impl Default for VotePolicy { - fn default() -> Self { - VotePolicy { - weight_kind: WeightKind::RoleWeight, - quorum: U128(0), - threshold: WeightOrRatio::Ratio(1, 2), - } - } -} - -/// Defines voting / decision making policy of this DAO. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde")] -pub struct Policy { - /// List of roles and permissions for them in the current policy. - pub roles: Vec, - /// Default vote policy. Used when given proposal kind doesn't have special policy. - pub default_vote_policy: VotePolicy, - /// Proposal bond. - pub proposal_bond: U128, - /// Expiration period for proposals. - pub proposal_period: WrappedDuration, - /// Bond for claiming a bounty. - pub bounty_bond: U128, - /// Period in which giving up on bounty is not punished. - pub bounty_forgiveness_period: WrappedDuration, -} - -/// Versioned policy. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] -#[serde(crate = "near_sdk::serde", untagged)] -pub enum VersionedPolicy { - /// Default policy with given accounts as council. - Default(Vec), - Current(Policy), -} - -/// Defines default policy: -/// - everyone can add proposals -/// - group consisting of the call can do all actions, consists of caller. -/// - non token weighted voting, requires 1/2 of the group to vote -/// - proposal & bounty bond is 1N -/// - proposal & bounty forgiveness period is 1 day -fn default_policy(council: Vec) -> Policy { - Policy { - roles: vec![ - RolePermission { - name: "all".to_string(), - kind: RoleKind::Everyone, - permissions: vec!["*:AddProposal".to_string()].into_iter().collect(), - vote_policy: HashMap::default(), - }, - RolePermission { - name: "council".to_string(), - kind: RoleKind::Group(council.into_iter().collect()), - // All actions except RemoveProposal are allowed by council. - permissions: vec![ - "*:AddProposal".to_string(), - "*:VoteApprove".to_string(), - "*:VoteReject".to_string(), - "*:VoteRemove".to_string(), - "*:Finalize".to_string(), - ] - .into_iter() - .collect(), - vote_policy: HashMap::default(), - }, - ], - default_vote_policy: VotePolicy::default(), - proposal_bond: U128(10u128.pow(24)), - proposal_period: WrappedDuration::from(1_000_000_000 * 60 * 60 * 24 * 7), - bounty_bond: U128(10u128.pow(24)), - bounty_forgiveness_period: WrappedDuration::from(1_000_000_000 * 60 * 60 * 24), - } -} - -impl VersionedPolicy { - /// Upgrades either version of policy into the latest. - pub fn upgrade(self) -> Self { - match self { - VersionedPolicy::Default(accounts) => { - VersionedPolicy::Current(default_policy(accounts)) - } - VersionedPolicy::Current(policy) => VersionedPolicy::Current(policy), - } - } - - /// Return recent version of policy. - pub fn to_policy(self) -> Policy { - match self { - VersionedPolicy::Current(policy) => policy, - _ => unimplemented!(), - } - } - - pub fn to_policy_mut(&mut self) -> &mut Policy { - match self { - VersionedPolicy::Current(policy) => policy, - _ => unimplemented!(), - } - } -} - -impl Policy { - /// - /// Doesn't fail, because will be used on the finalization of the proposal. - pub fn add_member_to_role(&mut self, role: &String, member_id: &AccountId) { - for i in 0..self.roles.len() { - if &self.roles[i].name == role { - self.roles[i] - .kind - .add_member_to_group(member_id) - .unwrap_or_else(|()| { - env::log(&format!("ERR_ROLE_WRONG_KIND:{}", role).into_bytes()); - }); - return; - } - } - env::log(&format!("ERR_ROLE_NOT_FOUND:{}", role).into_bytes()); - } - - pub fn remove_member_from_role(&mut self, role: &String, member_id: &AccountId) { - for i in 0..self.roles.len() { - if &self.roles[i].name == role { - self.roles[i] - .kind - .remove_member_from_group(member_id) - .unwrap_or_else(|()| { - env::log(&format!("ERR_ROLE_WRONG_KIND:{}", role).into_bytes()); - }); - return; - } - } - env::log(&format!("ERR_ROLE_NOT_FOUND:{}", role).into_bytes()); - } - - /// 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> { - let mut roles = HashMap::default(); - for role in self.roles.iter() { - if role.kind.match_user(&user) { - roles.insert(role.name.clone(), &role.permissions); - } - } - roles - } - - /// Can given user execute given action on this proposal. - /// Returns all roles that allow this action. - pub fn can_execute_action( - &self, - user: UserInfo, - proposal_kind: &ProposalKind, - action: &Action, - ) -> (Vec, bool) { - let roles = self.get_user_roles(user); - let mut allowed = false; - 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())) - || permissions.contains("*:*"); - allowed = allowed || allowed_role; - if allowed_role { - Some(role) - } else { - None - } - }) - .collect(); - (allowed_roles, allowed) - } - - /// Returns if given proposal kind is token weighted. - pub fn is_token_weighted(&self, role: &String, proposal_kind_label: &String) -> bool { - let role_info = self.internal_get_role(role).expect("ERR_ROLE_NOT_FOUND"); - match role_info - .vote_policy - .get(proposal_kind_label) - .unwrap_or(&self.default_vote_policy) - .weight_kind - { - WeightKind::TokenWeight => true, - _ => false, - } - } - - fn internal_get_role(&self, name: &String) -> Option<&RolePermission> { - for role in self.roles.iter() { - if role.name == *name { - return Some(role); - } - } - None - } - - /// Get proposal status for given proposal. - /// Usually is called after changing it's state. - pub fn proposal_status( - &self, - proposal: &Proposal, - roles: Vec, - total_supply: Balance, - ) -> ProposalStatus { - assert_eq!( - proposal.status, - ProposalStatus::InProgress, - "ERR_PROPOSAL_NOT_IN_PROGRESS" - ); - if proposal.submission_time.0 + self.proposal_period.0 < env::block_timestamp() { - // Proposal expired. - return ProposalStatus::Expired; - }; - for role in roles { - let role_info = self.internal_get_role(&role).expect("ERR_MISSING_ROLE"); - let vote_policy = role_info - .vote_policy - .get(&proposal.kind.to_policy_label().to_string()) - .unwrap_or(&self.default_vote_policy); - let threshold = std::cmp::max( - 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, - ), - }, - ); - // 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"); - if vote_counts[Vote::Approve as usize] >= threshold { - return ProposalStatus::Approved; - } else if vote_counts[Vote::Reject as usize] >= threshold { - return ProposalStatus::Rejected; - } else if vote_counts[Vote::Remove as usize] >= threshold { - return ProposalStatus::Removed; - } else { - // continue to next role. - } - } - proposal.status.clone() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vote_policy() { - let r1 = WeightOrRatio::Weight(U128(100)); - assert_eq!(r1.to_weight(1_000_000), 100); - let r2 = WeightOrRatio::Ratio(1, 2); - assert_eq!(r2.to_weight(2), 2); - let r2 = WeightOrRatio::Ratio(1, 2); - assert_eq!(r2.to_weight(5), 3); - let r2 = WeightOrRatio::Ratio(1, 1); - assert_eq!(r2.to_weight(5), 5); - } -} diff --git a/sputnikdao2-gasfix/src/proposals.rs b/sputnikdao2-gasfix/src/proposals.rs deleted file mode 100644 index 49de8b849..000000000 --- a/sputnikdao2-gasfix/src/proposals.rs +++ /dev/null @@ -1,511 +0,0 @@ -use std::collections::HashMap; -use std::convert::TryFrom; - -use near_contract_standards::fungible_token::core_impl::ext_fungible_token; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{Base64VecU8, WrappedTimestamp, U64}; -use near_sdk::{log, AccountId, Balance, PromiseOrValue}; - -use crate::policy::UserInfo; -use crate::types::{ - upgrade_remote, upgrade_self, Action, Config, BASE_TOKEN, GAS_FOR_FT_TRANSFER, ONE_YOCTO_NEAR, -}; -use crate::*; - -/// Status of a proposal. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, PartialEq, Debug)] -#[serde(crate = "near_sdk::serde")] -pub enum ProposalStatus { - InProgress, - /// If quorum voted yes, this proposal is successfully approved. - Approved, - /// If quorum voted no, this proposal is rejected. Bond is returned. - Rejected, - /// If quorum voted to remove (e.g. spam), this proposal is rejected and bond is not returned. - /// Interfaces shouldn't show removed proposals. - Removed, - /// Expired after period of time. - Expired, - /// If proposal was moved to Hub or somewhere else. - Moved, -} - -/// Function call arguments. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Clone, Debug))] -#[serde(crate = "near_sdk::serde")] -pub struct ActionCall { - method_name: String, - args: Base64VecU8, - deposit: U128, - gas: U64, -} - -/// Kinds of proposals, doing different action. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Clone, Debug))] -#[serde(crate = "near_sdk::serde")] -pub enum ProposalKind { - /// Change the DAO config. - ChangeConfig { config: Config }, - /// Change the full policy. - ChangePolicy { policy: VersionedPolicy }, - /// Add member to given role in the policy. This is short cut to updating the whole policy. - AddMemberToRole { - member_id: ValidAccountId, - role: String, - }, - /// Remove member to given role in the policy. This is short cut to updating the whole policy. - RemoveMemberFromRole { - member_id: ValidAccountId, - role: String, - }, - /// Calls `receiver_id` with list of method names in a single promise. - /// Allows this contract to execute any arbitrary set of actions in other contracts. - FunctionCall { - receiver_id: ValidAccountId, - actions: Vec, - }, - /// Upgrade this contract with given hash from blob store. - UpgradeSelf { hash: Base58CryptoHash }, - /// Upgrade another contract, by calling method with the code from given hash from blob store. - UpgradeRemote { - receiver_id: ValidAccountId, - method_name: String, - hash: Base58CryptoHash, - }, - /// Transfers given amount of `token_id` from this DAO to `receiver_id`. - /// If `msg` is not None, calls `ft_transfer_call` with given `msg`. Fails if this base token. - /// For `ft_transfer` and `ft_transfer_call` `memo` is the `description` of the proposal. - Transfer { - /// Can be "" for $NEAR or a valid account id. - token_id: AccountId, - receiver_id: ValidAccountId, - amount: U128, - msg: Option, - }, - /// Sets staking contract. Can only be proposed if staking contract is not set yet. - SetStakingContract { staking_id: ValidAccountId }, - /// Add new bounty. - AddBounty { bounty: Bounty }, - /// Indicates that given bounty is done by given user. - BountyDone { - bounty_id: u64, - receiver_id: ValidAccountId, - }, - /// Just a signaling vote, with no execution. - Vote, -} - -impl ProposalKind { - /// Returns label of policy for given type of proposal. - pub fn to_policy_label(&self) -> &str { - match self { - ProposalKind::ChangeConfig { .. } => "config", - ProposalKind::ChangePolicy { .. } => "policy", - ProposalKind::AddMemberToRole { .. } => "add_member_to_role", - ProposalKind::RemoveMemberFromRole { .. } => "remove_member_from_role", - ProposalKind::FunctionCall { .. } => "call", - ProposalKind::UpgradeSelf { .. } => "upgrade_self", - ProposalKind::UpgradeRemote { .. } => "upgrade_remote", - ProposalKind::Transfer { .. } => "transfer", - ProposalKind::SetStakingContract { .. } => "set_vote_token", - ProposalKind::AddBounty { .. } => "add_bounty", - ProposalKind::BountyDone { .. } => "bounty_done", - ProposalKind::Vote => "vote", - } - } -} - -/// Votes recorded in the proposal. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, Debug)] -#[serde(crate = "near_sdk::serde")] -pub enum Vote { - Approve = 0x0, - Reject = 0x1, - Remove = 0x2, -} - -impl From for Vote { - fn from(action: Action) -> Self { - match action { - Action::VoteApprove => Vote::Approve, - Action::VoteReject => Vote::Reject, - Action::VoteRemove => Vote::Remove, - _ => unreachable!(), - } - } -} - -/// Proposal that are sent to this DAO. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] -#[serde(crate = "near_sdk::serde")] -pub struct Proposal { - /// Original proposer. - pub proposer: AccountId, - /// Description of this proposal. - pub description: String, - /// Kind of proposal with relevant information. - pub kind: ProposalKind, - /// Current status of the proposal. - pub status: ProposalStatus, - /// Count of votes per role per decision: yes / no / spam. - pub vote_counts: HashMap, - /// Map of who voted and how. - pub votes: HashMap, - /// Submission time (for voting period). - pub submission_time: WrappedTimestamp, -} - -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] -#[serde(crate = "near_sdk::serde")] -pub enum VersionedProposal { - Default(Proposal), -} - -impl From for Proposal { - fn from(v: VersionedProposal) -> Self { - match v { - VersionedProposal::Default(p) => p, - } - } -} - -impl Proposal { - /// Adds vote of the given user with given `amount` of weight. If user already voted, fails. - pub fn update_votes( - &mut self, - account_id: &AccountId, - roles: &[String], - vote: Vote, - policy: &Policy, - user_weight: Balance, - ) { - for role in roles { - let amount = if policy.is_token_weighted(role, &self.kind.to_policy_label().to_string()) - { - user_weight - } else { - 1 - }; - self.vote_counts.entry(role.clone()).or_insert([0u128; 3])[vote.clone() as usize] += - amount; - } - assert!( - self.votes.insert(account_id.clone(), vote).is_none(), - "ERR_ALREADY_VOTED" - ); - } -} - -#[derive(Serialize, Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct ProposalInput { - /// Description of this proposal. - pub description: String, - /// Kind of proposal with relevant information. - pub kind: ProposalKind, -} - -impl From for Proposal { - fn from(input: ProposalInput) -> Self { - Self { - proposer: env::predecessor_account_id(), - description: input.description, - kind: input.kind, - status: ProposalStatus::InProgress, - vote_counts: HashMap::default(), - votes: HashMap::default(), - submission_time: WrappedTimestamp::from(env::block_timestamp()), - } - } -} - -impl Contract { - /// Execute payout of given token to given user. - pub(crate) fn internal_payout( - &mut self, - token_id: &AccountId, - receiver_id: &AccountId, - amount: Balance, - memo: String, - msg: Option, - ) -> PromiseOrValue<()> { - if token_id == BASE_TOKEN { - Promise::new(receiver_id.clone()).transfer(amount).into() - } else { - if let Some(msg) = msg { - ext_fungible_token::ft_transfer_call( - receiver_id.clone(), - U128(amount), - Some(memo), - msg, - &token_id, - ONE_YOCTO_NEAR, - GAS_FOR_FT_TRANSFER, - ) - .into() - } else { - ext_fungible_token::ft_transfer( - receiver_id.clone(), - U128(amount), - Some(memo), - &token_id, - ONE_YOCTO_NEAR, - GAS_FOR_FT_TRANSFER, - ) - .into() - } - } - } - - /// Executes given proposal and updates the contract's state. - fn internal_execute_proposal( - &mut self, - policy: &Policy, - proposal: &Proposal, - ) -> PromiseOrValue<()> { - // Return the proposal bond. - Promise::new(proposal.proposer.clone()).transfer(policy.proposal_bond.0); - match &proposal.kind { - ProposalKind::ChangeConfig { config } => { - self.config.set(config); - PromiseOrValue::Value(()) - } - ProposalKind::ChangePolicy { policy } => { - self.policy.set(policy); - PromiseOrValue::Value(()) - } - ProposalKind::AddMemberToRole { member_id, role } => { - let mut new_policy = policy.clone(); - new_policy.add_member_to_role(role, &member_id.clone().into()); - self.policy.set(&VersionedPolicy::Current(new_policy)); - PromiseOrValue::Value(()) - } - ProposalKind::RemoveMemberFromRole { member_id, role } => { - let mut new_policy = policy.clone(); - new_policy.remove_member_from_role(role, &member_id.clone().into()); - self.policy.set(&VersionedPolicy::Current(new_policy)); - PromiseOrValue::Value(()) - } - ProposalKind::FunctionCall { - receiver_id, - actions, - } => { - let mut promise = Promise::new(receiver_id.clone().into()); - for action in actions { - promise = promise.function_call( - action.method_name.clone().into_bytes(), - action.args.clone().into(), - action.deposit.0, - action.gas.0, - ) - } - promise.into() - } - ProposalKind::UpgradeSelf { hash } => { - upgrade_self(&CryptoHash::from(hash.clone())); - PromiseOrValue::Value(()) - } - ProposalKind::UpgradeRemote { - receiver_id, - method_name, - hash, - } => { - upgrade_remote( - &receiver_id.clone().into(), - method_name, - &CryptoHash::from(hash.clone()), - ); - PromiseOrValue::Value(()) - } - ProposalKind::Transfer { - token_id, - receiver_id, - amount, - msg, - } => self.internal_payout( - token_id, - &receiver_id.clone().into(), - amount.0, - proposal.description.clone(), - msg.clone(), - ), - ProposalKind::SetStakingContract { staking_id } => { - assert!(self.staking_id.is_none(), "ERR_INVALID_STAKING_CHANGE"); - self.staking_id = Some(staking_id.clone().into()); - PromiseOrValue::Value(()) - } - ProposalKind::AddBounty { bounty } => { - self.internal_add_bounty(bounty); - PromiseOrValue::Value(()) - } - ProposalKind::BountyDone { - bounty_id, - receiver_id, - } => self.internal_execute_bounty_payout(*bounty_id, &receiver_id.clone().into(), true), - ProposalKind::Vote => PromiseOrValue::Value(()), - } - } - - /// Process rejecting proposal. - fn internal_reject_proposal( - &mut self, - policy: &Policy, - proposal: &Proposal, - return_bond: bool, - ) -> PromiseOrValue<()> { - if return_bond { - // Return bond to the proposer. - Promise::new(proposal.proposer.clone()).transfer(policy.proposal_bond.0); - } - match &proposal.kind { - ProposalKind::BountyDone { - bounty_id, - receiver_id, - } => { - self.internal_execute_bounty_payout(*bounty_id, &receiver_id.clone().into(), false) - } - _ => PromiseOrValue::Value(()), - } - } - - pub(crate) fn internal_user_info(&self) -> UserInfo { - let account_id = env::predecessor_account_id(); - UserInfo { - amount: self.get_user_weight(&account_id), - account_id, - } - } -} - -#[near_bindgen] -impl Contract { - /// Add proposal to this DAO. - #[payable] - pub fn add_proposal(&mut self, proposal: ProposalInput) -> u64 { - // 0. validate bond attached. - // TODO: consider bond in the token of this DAO. - let policy = self.policy.get().unwrap().to_policy(); - assert!( - env::attached_deposit() >= policy.proposal_bond.0, - "ERR_MIN_BOND" - ); - - // 1. Validate proposal. - match &proposal.kind { - ProposalKind::Transfer { token_id, msg, .. } => { - assert!( - !(token_id == BASE_TOKEN) || msg.is_none(), - "ERR_BASE_TOKEN_NO_MSG" - ); - if token_id != BASE_TOKEN { - assert!( - ValidAccountId::try_from(token_id.clone()).is_ok(), - "ERR_TOKEN_ID_INVALID" - ); - } - } - ProposalKind::SetStakingContract { .. } => assert!( - self.staking_id.is_none(), - "ERR_STAKING_CONTRACT_CANT_CHANGE" - ), - // TODO: add more verifications. - _ => {} - }; - - // 2. Check permission of caller to add this type of proposal. - assert!( - policy - .can_execute_action( - self.internal_user_info(), - &proposal.kind, - &Action::AddProposal - ) - .1, - "ERR_PERMISSION_DENIED" - ); - - // 3. Actually add proposal to the current list of proposals. - let id = self.last_proposal_id; - self.proposals - .insert(&id, &VersionedProposal::Default(proposal.into())); - self.last_proposal_id += 1; - id - } - - /// 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 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"); - let sender_id = env::predecessor_account_id(); - // Update proposal given action. Returns true if should be updated in storage. - let update = match action { - Action::AddProposal => env::panic(b"ERR_WRONG_ACTION"), - Action::RemoveProposal => { - self.proposals.remove(&id); - false - } - Action::VoteApprove | Action::VoteReject | Action::VoteRemove => { - assert_eq!( - proposal.status, - ProposalStatus::InProgress, - "ERR_PROPOSAL_NOT_IN_PROGRESS" - ); - proposal.update_votes( - &sender_id, - &roles, - Vote::from(action), - &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 { - 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 - } - } - Action::Finalize => { - proposal.status = policy.proposal_status( - &proposal, - 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 - } - Action::MoveToHub => false, - }; - if update { - self.proposals - .insert(&id, &VersionedProposal::Default(proposal)); - } - if let Some(memo) = memo { - log!("Memo: {}", memo); - } - } -} diff --git a/sputnikdao2-gasfix/src/types.rs b/sputnikdao2-gasfix/src/types.rs deleted file mode 100644 index 234a29423..000000000 --- a/sputnikdao2-gasfix/src/types.rs +++ /dev/null @@ -1,144 +0,0 @@ -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::env::BLOCKCHAIN_INTERFACE; -use near_sdk::json_types::Base64VecU8; -use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{env, AccountId, Balance, Gas}; - -const BLOCKCHAIN_INTERFACE_NOT_SET_ERR: &str = "Blockchain interface not set."; - -/// Account ID used for $NEAR. -pub const BASE_TOKEN: &str = ""; - -/// 1 yN to prevent access key fraud. -pub const ONE_YOCTO_NEAR: Balance = 1; - -/// Gas for single ft_transfer call. -pub const GAS_FOR_FT_TRANSFER: Gas = 10_000_000_000_000; - -/// Gas for upgrading this contract on promise creation + deploying new contract. -// pub const GAS_FOR_UPGRADE_SELF_DEPLOY: Gas = 30_000_000_000_000; -pub const GAS_FOR_UPGRADE_SELF_DEPLOY: Gas = 200_000_000_000_000; - -// pub const GAS_FOR_UPGRADE_REMOTE_DEPLOY: Gas = 10_000_000_000_000; -pub const GAS_FOR_UPGRADE_REMOTE_DEPLOY: Gas = 200_000_000_000_000; - -/// Configuration of the DAO. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, Debug)] -#[serde(crate = "near_sdk::serde")] -pub struct Config { - /// Name of the DAO. - pub name: String, - /// Purpose of this DAO. - pub purpose: String, - /// Generic metadata. Can be used by specific UI to store additional data. - /// This is not used by anything in the contract. - pub metadata: Base64VecU8, -} - -#[cfg(test)] -impl Config { - pub fn test_config() -> Self { - Self { - name: "Test".to_string(), - purpose: "to test".to_string(), - metadata: Base64VecU8(vec![]), - } - } -} - -/// Set of possible action to take. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug)] -#[serde(crate = "near_sdk::serde")] -pub enum Action { - /// Action to add proposal. Used internally. - AddProposal, - /// Action to remove given proposal. Used for immediate deletion in special cases. - RemoveProposal, - /// Vote to approve given proposal or bounty. - VoteApprove, - /// Vote to reject given proposal or bounty. - VoteReject, - /// Vote to remove given proposal or bounty (because it's spam). - VoteRemove, - /// Finalize proposal, called when it's expired to return the funds - /// (or in the future can be used for early proposal closure). - Finalize, - /// Move a proposal to the hub to shift into another DAO. - MoveToHub, -} - -impl Action { - pub fn to_policy_label(&self) -> String { - format!("{:?}", self) - } -} - -/// Self upgrade, optimizes gas by not loading into memory the code. -pub(crate) fn upgrade_self(hash: &[u8]) { - let current_id = env::current_account_id().into_bytes(); - let method_name = "migrate".as_bytes().to_vec(); - let attached_gas = env::prepaid_gas() - env::used_gas() - GAS_FOR_UPGRADE_SELF_DEPLOY; - unsafe { - BLOCKCHAIN_INTERFACE.with(|b| { - // Load input (wasm code) into register 0. - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .storage_read(hash.len() as _, hash.as_ptr() as _, 0); - // schedule a Promise tx to this same contract - let promise_id = b - .borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .promise_batch_create(current_id.len() as _, current_id.as_ptr() as _); - // 1st item in the Tx: "deploy contract" (code is taken from register 0) - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .promise_batch_action_deploy_contract(promise_id, u64::MAX as _, 0); - // 2nd item in the Tx: call this_contract.migrate() with remaining gas - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .promise_batch_action_function_call( - promise_id, - method_name.len() as _, - method_name.as_ptr() as _, - 0 as _, - 0 as _, - 0 as _, - attached_gas, - ); - }); - } -} - -pub(crate) fn upgrade_remote(receiver_id: &AccountId, method_name: &str, hash: &[u8]) { - unsafe { - BLOCKCHAIN_INTERFACE.with(|b| { - // Load input into register 0. - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .storage_read(hash.len() as _, hash.as_ptr() as _, 0); - let promise_id = b - .borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .promise_batch_create(receiver_id.len() as _, receiver_id.as_ptr() as _); - let attached_gas = env::prepaid_gas() - env::used_gas() - GAS_FOR_UPGRADE_REMOTE_DEPLOY; - b.borrow() - .as_ref() - .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) - .promise_batch_action_function_call( - promise_id, - method_name.len() as _, - method_name.as_ptr() as _, - u64::MAX as _, - 0 as _, - 0 as _, - attached_gas, - ); - }); - } -} diff --git a/sputnikdao2-gasfix/src/views.rs b/sputnikdao2-gasfix/src/views.rs deleted file mode 100644 index b1bbca0f0..000000000 --- a/sputnikdao2-gasfix/src/views.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::cmp::min; - -use crate::*; - -/// This is format of output via JSON for the proposal. -#[derive(Serialize, Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct ProposalOutput { - /// Id of the proposal. - pub id: u64, - #[serde(flatten)] - pub proposal: Proposal, -} - -/// This is format of output via JSON for the bounty. -#[derive(Serialize, Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct BountyOutput { - /// Id of the bounty. - pub id: u64, - #[serde(flatten)] - pub bounty: Bounty, -} - -#[near_bindgen] -impl Contract { - /// Returns semver of this contract. - pub fn version(&self) -> String { - env!("CARGO_PKG_VERSION").to_string() - } - - /// Returns config of this contract. - pub fn get_config(&self) -> Config { - self.config.get().unwrap().clone() - } - - /// Returns policy of this contract. - pub fn get_policy(&self) -> Policy { - self.policy.get().unwrap().to_policy().clone() - } - - /// Returns staking contract if available. Otherwise returns empty. - pub fn get_staking_contract(&self) -> AccountId { - self.staking_id.clone().unwrap_or_default() - } - - /// Returns if blob with given hash is stored. - pub fn has_blob(&self, hash: Base58CryptoHash) -> bool { - env::storage_read(&CryptoHash::from(hash)).is_some() - } - - /// Returns available amount of NEAR that can be spent (outside of amount for storage and bonds). - pub fn get_available_amount(&self) -> U128 { - U128(env::account_balance() - self.locked_amount) - } - - /// Returns total delegated stake. - pub fn delegation_total_supply(&self) -> U128 { - U128(self.total_delegation_amount) - } - - /// Returns delegated stake to given account. - pub fn delegation_balance_of(&self, account_id: ValidAccountId) -> U128 { - U128( - self.delegations - .get(account_id.as_ref()) - .unwrap_or_default(), - ) - } - - /// Last proposal's id. - pub fn get_last_proposal_id(&self) -> u64 { - self.last_proposal_id - } - - /// Get proposals in paginated view. - pub fn get_proposals(&self, from_index: u64, limit: u64) -> Vec { - (from_index..min(self.last_proposal_id, from_index + limit)) - .filter_map(|id| { - self.proposals.get(&id).map(|proposal| ProposalOutput { - id, - proposal: proposal.into(), - }) - }) - .collect() - } - - /// Get specific proposal. - pub fn get_proposal(&self, id: u64) -> ProposalOutput { - let proposal = self.proposals.get(&id).expect("ERR_NO_PROPOSAL"); - ProposalOutput { - id, - proposal: proposal.into(), - } - } - - /// Get given bounty by id. - pub fn get_bounty(&self, id: u64) -> BountyOutput { - let bounty = self.bounties.get(&id).expect("ERR_NO_BOUNTY"); - BountyOutput { - id, - bounty: bounty.into(), - } - } - - /// Get number of bounties. - pub fn get_last_bounty_id(&self) -> u64 { - self.last_bounty_id - } - - /// Get `limit` of bounties from given index. - pub fn get_bounties(&self, from_index: u64, limit: u64) -> Vec { - (from_index..std::cmp::min(from_index + limit, self.last_bounty_id)) - .filter_map(|id| { - self.bounties.get(&id).map(|bounty| BountyOutput { - id, - bounty: bounty.into(), - }) - }) - .collect() - } - - /// Get bounty claims for given user. - pub fn get_bounty_claims(&self, account_id: ValidAccountId) -> Vec { - self.bounty_claimers - .get(account_id.as_ref()) - .unwrap_or_default() - } - - /// Returns number of claims per given bounty. - pub fn get_bounty_number_of_claims(&self, id: u64) -> u32 { - self.bounty_claims_count.get(&id).unwrap_or_default() - } -} diff --git a/sputnikdao2-gasfix/tests/test_general.rs b/sputnikdao2-gasfix/tests/test_general.rs deleted file mode 100644 index 4e93d0003..000000000 --- a/sputnikdao2-gasfix/tests/test_general.rs +++ /dev/null @@ -1,244 +0,0 @@ -use std::collections::HashMap; - -use near_sdk::json_types::U128; -use near_sdk::AccountId; -use near_sdk_sim::{call, to_yocto, view}; - -use sputnik_staking::User; -use sputnikdao2::{ - Action, Policy, Proposal, ProposalInput, ProposalKind, ProposalStatus, RoleKind, - RolePermission, VersionedPolicy, VotePolicy, -}; - -use crate::utils::*; - -mod utils; - -fn user(id: u32) -> String { - format!("user{}", id) -} - -#[test] -fn test_multi_council() { - let (root, dao) = setup_dao(); - let user1 = root.create_user(user(1), to_yocto("1000")); - let user2 = root.create_user(user(2), to_yocto("1000")); - let user3 = root.create_user(user(3), to_yocto("1000")); - let new_policy = Policy { - roles: vec![ - RolePermission { - name: "all".to_string(), - kind: RoleKind::Everyone, - permissions: vec!["*:AddProposal".to_string()].into_iter().collect(), - vote_policy: HashMap::default(), - }, - RolePermission { - name: "council".to_string(), - kind: RoleKind::Group(vec![user(1), user(2)].into_iter().collect()), - permissions: vec!["*:*".to_string()].into_iter().collect(), - vote_policy: HashMap::default(), - }, - RolePermission { - name: "community".to_string(), - kind: RoleKind::Group(vec![user(1), user(3), user(4)].into_iter().collect()), - permissions: vec!["*:*".to_string()].into_iter().collect(), - vote_policy: HashMap::default(), - }, - ], - default_vote_policy: VotePolicy::default(), - proposal_bond: U128(10u128.pow(24)), - proposal_period: WrappedDuration::from(1_000_000_000 * 60 * 60 * 24 * 7), - bounty_bond: U128(10u128.pow(24)), - bounty_forgiveness_period: WrappedDuration::from(1_000_000_000 * 60 * 60 * 24), - }; - add_proposal( - &root, - &dao, - ProposalInput { - description: "new policy".to_string(), - kind: ProposalKind::ChangePolicy { - policy: VersionedPolicy::Current(new_policy.clone()), - }, - }, - ) - .assert_success(); - vote(vec![&root], &dao, 0); - assert_eq!(view!(dao.get_policy()).unwrap_json::(), new_policy); - add_transfer_proposal(&root, &dao, base_token(), user(1), 1_000_000, None).assert_success(); - vote(vec![&user2], &dao, 1); - vote(vec![&user3], &dao, 1); - let proposal = view!(dao.get_proposal(1)).unwrap_json::(); - // Votes from members in different councils. - assert_eq!(proposal.status, ProposalStatus::InProgress); - // Finish with vote that is in both councils, which approves the proposal. - vote(vec![&user1], &dao, 1); - let proposal = view!(dao.get_proposal(1)).unwrap_json::(); - assert_eq!(proposal.status, ProposalStatus::Approved); -} - -#[test] -fn test_create_dao_and_use_token() { - 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 test_token = setup_test_token(&root); - let staking = setup_staking(&root); - - assert!(view!(dao.get_staking_contract()) - .unwrap_json::() - .is_empty()); - 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. - should_fail(call!(user2, dao.act_proposal(0, Action::VoteApprove, None))); - call!(root, dao.act_proposal(0, Action::VoteApprove, None)).assert_success(); - // voting second time should fail. - should_fail(call!(root, dao.act_proposal(0, Action::VoteApprove, None))); - // Add 3rd member. - add_member_proposal(&user2, &dao, user3.account_id.clone()).assert_success(); - vote(vec![&root, &user2], &dao, 1); - let policy = view!(dao.get_policy()).unwrap_json::(); - assert_eq!(policy.roles.len(), 2); - assert_eq!( - policy.roles[1].kind, - RoleKind::Group( - vec![ - root.account_id.clone(), - user2.account_id.clone(), - user3.account_id.clone() - ] - .into_iter() - .collect() - ) - ); - add_proposal( - &user2, - &dao, - ProposalInput { - description: "test".to_string(), - kind: ProposalKind::SetStakingContract { - staking_id: to_va("staking".to_string()), - }, - }, - ) - .assert_success(); - vote(vec![&user3, &user2], &dao, 2); - assert!(!view!(dao.get_staking_contract()) - .unwrap_json::() - .is_empty()); - assert_eq!( - view!(dao.get_proposal(2)).unwrap_json::().status, - ProposalStatus::Approved - ); - assert_eq!( - view!(staking.ft_total_supply()).unwrap_json::().0, - to_yocto("0") - ); - call!( - user2, - test_token.mint(to_va(user2.account_id.clone()), U128(to_yocto("100"))) - ) - .assert_success(); - call!( - user2, - test_token.storage_deposit(Some(to_va(staking.account_id())), None), - deposit = to_yocto("1") - ) - .assert_success(); - call!( - user2, - staking.storage_deposit(None, None), - deposit = to_yocto("1") - ); - call!( - user2, - test_token.ft_transfer_call( - to_va(staking.account_id()), - U128(to_yocto("10")), - None, - "".to_string() - ), - deposit = 1 - ) - .assert_success(); - assert_eq!( - view!(staking.ft_total_supply()).unwrap_json::().0, - to_yocto("10") - ); - let user2_id = to_va(user2.account_id.clone()); - assert_eq!( - view!(staking.ft_balance_of(user2_id.clone())) - .unwrap_json::() - .0, - to_yocto("10") - ); - assert_eq!( - view!(test_token.ft_balance_of(user2_id.clone())) - .unwrap_json::() - .0, - to_yocto("90") - ); - call!(user2, staking.withdraw(U128(to_yocto("5")))).assert_success(); - assert_eq!( - view!(staking.ft_total_supply()).unwrap_json::().0, - to_yocto("5") - ); - assert_eq!( - view!(test_token.ft_balance_of(user2_id.clone())) - .unwrap_json::() - .0, - to_yocto("95") - ); - call!( - user2, - staking.delegate(user2_id.clone(), U128(to_yocto("5"))) - ) - .assert_success(); - call!( - user2, - staking.undelegate(user2_id.clone(), U128(to_yocto("1"))) - ) - .assert_success(); - // should fail right after undelegation as need to wait for voting period before can delegate again. - should_fail(call!( - user2, - staking.delegate(user2_id.clone(), U128(to_yocto("1"))) - )); - let user = view!(staking.get_user(user2_id.clone())).unwrap_json::(); - assert_eq!( - user.delegated_amounts, - vec![(user2_id.to_string(), U128(to_yocto("4")))] - ); - assert_eq!( - view!(dao.delegation_total_supply()).unwrap_json::().0, - to_yocto("4") - ); - assert_eq!( - view!(dao.delegation_balance_of(user2_id)) - .unwrap_json::() - .0, - to_yocto("4") - ); -} - -/// Test various cases that must fail. -#[test] -fn test_failures() { - let (root, dao) = setup_dao(); - should_fail(add_transfer_proposal( - &root, - &dao, - base_token(), - user(1), - 1_000_000, - Some("some".to_string()), - )); - should_fail(add_transfer_proposal( - &root, - &dao, - "not:a^valid.token@".to_string(), - user(1), - 1_000_000, - None, - )); -} diff --git a/sputnikdao2-gasfix/tests/test_upgrade.rs b/sputnikdao2-gasfix/tests/test_upgrade.rs deleted file mode 100644 index 3653b6034..000000000 --- a/sputnikdao2-gasfix/tests/test_upgrade.rs +++ /dev/null @@ -1,92 +0,0 @@ -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{Base58CryptoHash, ValidAccountId}; -use near_sdk::serde_json::json; -use near_sdk_sim::{call, to_yocto, view, DEFAULT_GAS}; -use sputnikdao2::{Action, ProposalInput, ProposalKind}; - -mod utils; -use crate::utils::*; - -near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { - DAO_WASM_BYTES => "res/sputnikdao2.wasm", - OTHER_WASM_BYTES => "res/ref_exchange_release.wasm" -} - -#[test] -fn test_upgrade() { - let (root, dao) = setup_dao(); - let hash = root - .call( - dao.user_account.account_id.clone(), - "store_blob", - &DAO_WASM_BYTES, - near_sdk_sim::DEFAULT_GAS, - to_yocto("200"), - ) - .unwrap_json::(); - call!( - root, - dao.add_proposal(ProposalInput { - description: "test".to_string(), - kind: ProposalKind::UpgradeSelf { hash } - }), - deposit = to_yocto("1") - ) - .assert_success(); - assert_eq!(view!(dao.get_last_proposal_id()).unwrap_json::(), 1); - call!(root, dao.act_proposal(0, Action::VoteApprove, None)).assert_success(); - assert_eq!(view!(dao.version()).unwrap_json::(), "2.0.0"); - call!(root, dao.remove_blob(hash)).assert_success(); - should_fail(call!(root, dao.remove_blob(hash))); -} - -#[derive(BorshSerialize, BorshDeserialize)] -struct NewArgs { - owner_id: ValidAccountId, - exchange_fee: u32, - referral_fee: u32, -} - -/// Test that Sputnik can upgrade another contract. -#[test] -fn test_upgrade_other() { - let (root, dao) = setup_dao(); - let ref_account_id = "ref-finance".to_string(); - let _ = root.deploy_and_init( - &OTHER_WASM_BYTES, - ref_account_id.clone(), - "new", - &json!({ - "owner_id": to_va(dao.account_id()), - "exchange_fee": 1, - "referral_fee": 1, - }) - .to_string() - .into_bytes(), - to_yocto("1000"), - DEFAULT_GAS, - ); - let hash = root - .call( - dao.user_account.account_id.clone(), - "store_blob", - &OTHER_WASM_BYTES, - near_sdk_sim::DEFAULT_GAS, - to_yocto("200"), - ) - .unwrap_json::(); - add_proposal( - &root, - &dao, - ProposalInput { - description: "test".to_string(), - kind: ProposalKind::UpgradeRemote { - receiver_id: to_va(ref_account_id.clone()), - method_name: "upgrade".to_string(), - hash, - }, - }, - ) - .assert_success(); - call!(root, dao.act_proposal(0, Action::VoteApprove, None)).assert_success(); -} diff --git a/sputnikdao2-gasfix/tests/utils/mod.rs b/sputnikdao2-gasfix/tests/utils/mod.rs deleted file mode 100644 index b86e1de90..000000000 --- a/sputnikdao2-gasfix/tests/utils/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -#![allow(dead_code)] -use std::convert::TryFrom; - -pub use near_sdk::json_types::{Base64VecU8, ValidAccountId, WrappedDuration, U64}; -use near_sdk::{AccountId, Balance}; -use near_sdk_sim::transaction::ExecutionStatus; -use near_sdk_sim::{ - call, deploy, init_simulator, to_yocto, ContractAccount, ExecutionResult, UserAccount, -}; - -use near_sdk::json_types::U128; -use sputnik_staking::ContractContract as StakingContract; -use sputnikdao2::{ - Action, Config, ContractContract as DAOContract, ProposalInput, ProposalKind, VersionedPolicy, -}; -use test_token::ContractContract as TestTokenContract; - -near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { - DAO_WASM_BYTES => "res/sputnikdao2.wasm", - TEST_TOKEN_WASM_BYTES => "../test-token/res/test_token.wasm", - STAKING_WASM_BYTES => "../sputnik-staking/res/sputnik_staking.wasm", -} - -type Contract = ContractAccount; - -pub fn base_token() -> String { - "".to_string() -} - -pub fn should_fail(r: ExecutionResult) { - match r.status() { - ExecutionStatus::Failure(_) => {} - _ => panic!("Should fail"), - } -} - -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![]), - }; - let dao = deploy!( - contract: DAOContract, - contract_id: "dao".to_string(), - bytes: &DAO_WASM_BYTES, - signer_account: root, - deposit: to_yocto("200"), - init_method: new(config, VersionedPolicy::Default(vec![root.account_id.clone()])) - ); - (root, dao) -} - -pub fn setup_test_token(root: &UserAccount) -> ContractAccount { - deploy!( - contract: TestTokenContract, - contract_id: "test_token".to_string(), - bytes: &TEST_TOKEN_WASM_BYTES, - signer_account: root, - deposit: to_yocto("200"), - init_method: new() - ) -} - -pub fn setup_staking(root: &UserAccount) -> ContractAccount { - deploy!( - contract: StakingContract, - contract_id: "staking".to_string(), - bytes: &STAKING_WASM_BYTES, - signer_account: root, - deposit: to_yocto("100"), - init_method: new(to_va("dao".to_string()), to_va("test_token".to_string()), U64(100_000_000_000)) - ) -} - -pub fn add_proposal( - root: &UserAccount, - dao: &Contract, - proposal: ProposalInput, -) -> ExecutionResult { - call!(root, dao.add_proposal(proposal), deposit = to_yocto("1")) -} - -pub fn add_member_proposal( - root: &UserAccount, - dao: &Contract, - member_id: AccountId, -) -> ExecutionResult { - add_proposal( - root, - dao, - ProposalInput { - description: "test".to_string(), - kind: ProposalKind::AddMemberToRole { - member_id: to_va(member_id), - role: "council".to_string(), - }, - }, - ) -} - -pub fn add_transfer_proposal( - root: &UserAccount, - dao: &Contract, - token_id: AccountId, - receiver_id: AccountId, - amount: Balance, - msg: Option, -) -> ExecutionResult { - add_proposal( - root, - dao, - ProposalInput { - description: "test".to_string(), - kind: ProposalKind::Transfer { - token_id, - receiver_id: to_va(receiver_id), - amount: U128(amount), - msg, - }, - }, - ) -} - -pub fn vote(users: Vec<&UserAccount>, dao: &Contract, proposal_id: u64) { - for user in users.into_iter() { - call!( - user, - dao.act_proposal(proposal_id, Action::VoteApprove, None) - ) - .assert_success(); - } -} - -pub fn to_va(a: AccountId) -> ValidAccountId { - ValidAccountId::try_from(a).unwrap() -} diff --git a/sputnikdao2/res/sputnikdao2.wasm b/sputnikdao2/res/sputnikdao2.wasm index aa857dae8..27ac48a5c 100755 Binary files a/sputnikdao2/res/sputnikdao2.wasm and b/sputnikdao2/res/sputnikdao2.wasm differ