diff --git a/.gitignore b/.gitignore index 6cbfeaf..1825630 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ neardev .DS_Store res/ref_token_local.wasm -res/xref_token_local.wasm \ No newline at end of file +res/xref_token_local.wasm +res/referendum_local.wasm \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index bf10d78..b98794d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "./ref-token", "./xref-token", + "./referendum", "./test-token" ] diff --git a/README.md b/README.md index 08cba9a..9a872db 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,6 @@ Basic Ref token ### XRef Token Ref Share Token + +### referendum +A referendum DAO for XREF token diff --git a/referendum/Cargo.toml b/referendum/Cargo.toml new file mode 100644 index 0000000..904340c --- /dev/null +++ b/referendum/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "referendum" +version = "1.0.0" +authors = ["Marco "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +uint = { version = "0.9.0", default-features = false } +near-sdk = "3.1.0" +near-contract-standards = "3.1.0" + +[dev-dependencies] +near-sdk-sim = "3.1.0" +test-token = { path = "../test-token" } \ No newline at end of file diff --git a/referendum/README.md b/referendum/README.md new file mode 100644 index 0000000..a5f5e34 --- /dev/null +++ b/referendum/README.md @@ -0,0 +1,238 @@ +# Referendum + +Detailed discussion about Referendum, see this [post](https://gov.ref.finance/t/product-x-referendum/384). + +## Instructions + +### Initialization +After deploy, we can initiate the contract as following: +```bash +near call $REFERENDUM new '{"owner_id": "'$OWNER'", "token_id": "'$TOKEN'"}' --account_id $REFERENDUM +``` +Then owner can determine a launch date and set it into contract in 30 days after deployment: +```bash +# set 2022-02-01 00:00:00 UTC to be genesis time +near call $REFERENDUM modify_genesis_timestamp '{"genesis_timestamp_in_sec": 1643673600}' --account_id $OWNER +``` + +### Owner Methods +Ownership can be transfered: +```bash +near view $REFERENDUM get_owner +near call $REFERENDUM set_owner '{"owner_id": "'$NEW_OWNER'"}' --account_id $OWNER +``` +Owner can set launch date, see Initialization for details. + +Owner can modify endorsement NEAR amount per proposal: +```bash +# set endorsement NEAR amount to 15 NEAR. +near call $REFERENDUM modify_endorsement_amount '{"amount": "15'$ZERO24'"}' --account_id $OWNER +``` + +Owner can set nonsense threshold: +```bash +# set threshold to 40% +near call $REFERENDUM modify_nonsense_threshold '{"threshold": {"numerator": 40, "denominator": 100}}' --account_id $OWNER +``` + +Owner can set vote policy: +```bash +# set relative policy to 30% voting ballot and 50%+ supported opinion wins +near call $REFERENDUM modify_vote_policy '{"vote_policy": {"Relative": [{"numerator": 30, "denominator": 100}, {"numerator": 1, "denominator": 2}]}}' --account_id $OWNER + +# set absolute policy to pass with 55%+ ballot power and fail with 45%+ ballot power +near call $REFERENDUM modify_vote_policy '{"vote_policy": {"Absolute": [{"numerator": 55, "denominator": 100}, {"numerator": 45, "denominator": 100}]}}' --account_id $OWNER +``` + +### Proposer and Proposals +Anyone can initiate a referendum proposal with fixed amount of NEAR as endorsement: +```bash +# alice.near deposit 15 NEAR as endorsement to create a referendum, +# referendum will start at 7 days (604800) after beginning of session 0, and lasts 14 days (1209600), +# The vote policy is Absolute (the other is Relative), +# Currently there is only one kind of referendum, Vote. +near call $REFERENDUM add_proposal '{"description": "example referendum, see detail at https://xxxxxxx", "kind": "Vote", "policy_type": "Absolute", "session_id": 0, "start_offset_sec": 604800, "lasts_sec": 1209600}' --account_id=alice.near --amount 15 +``` +The deposit NEAR would lock until the referendum goes to a final state, that is one of Approved, Rejected, Nonsense or Expired. + +On approved and Rejected state, the locked NEAR would auto transfer back to proposer; +On nonsense state, the locked NEAR would be slashed; +On expired state, proposer need to explicit call to redeem the locked NEAR: +```bash +# it returns true when succeed +near call $REFERENDUM redeem_near_in_expired_proposal '{"id": 0}' --account_id=alice.near +``` + +The proposer can also remove his proposal before it starts and gets locked NEAR back: +```bash +# it returns true when succeed +near call $REFERENDUM remove_proposal '{"id": 0}' +``` + +### User Register +This contract obeys NEP-145 to manage storage, but choose a fixed storage fee policy in this contract. Each user only needs deposit to lock a fixed 0.01 NEAR as storage cost. + +Detailed interface description could be found at [NEP-145](https://nomicon.io/Standards/StorageManagement.html). + +Here we only list some common-use interfaces: + +* `storage_deposit`, to register a user, +* `storage_unregister`, to unregister caller self and get 0.01 NEAR back, +* `storage_balance_of`, to get given user storage balance. + + +### User Methods +To lock token (XREF) and get ballot power, user need start from token contract: +```bash +# alice lock 100 TOKEN for 9 sessions +near call $TOKEN ft_tranfser_call '{"receiver_id": "'$REFERENDUM'", "amount": "100'$ZERO18'", "msg": "9"}' --account_id=alice.near --amount=$YN +``` +*Note: user can only start a new lock when there is no existing locking at his account.* + +When there is an existing locking, user can append token to it: +```bash +# alice append 100 TOKEN to his existing locking +near call $TOKEN ft_tranfser_call '{"receiver_id": "'$REFERENDUM'", "amount": "100'$ZERO18'", "msg": ""}' --account_id=alice.near --amount=$YN +``` +*Note: user can only append lock when there is a existing locking at his account.* + +To withdraw token when they are unlocked, user call: +```bash +near call $REFERENDUM withdraw --account_id=alice.near --amount=$YN +``` +*Note: If user wanna those token to be part of a new locking, he can directly start `ft_transfer_call` without withdrawing them first. those un-withdraw amount would auto caculate into the total locking amount.* + +User can vote any InProgress referendum: +```bash +# action could be one of VoteApprove, VoteReject, VoteNonsense +near call $REFERENDUM act_proposal '{"id": 0, "action": "VoteApprove"}' --account_id=alice.near +``` +*Note: user can only vote once per proposal and the ballot power would auto renew if user append locking more token and get more ballot.* + +### View Methods +#### **To view contract info:** +```bash +near view $REFERENDUM contract_metadata +``` +The return value structure is: +```rust +pub struct ContractMetadata { + /// the owner account id of contract + pub owner_id: AccountId, + /// accept lock token account id + pub locked_token: AccountId, + /// the launch timestamp in seconds + pub genesis_timestamp_sec: u32, + /// current session id (start from 0) + pub cur_session_id: u32, + /// current total ballot amount (calculate at call time) + pub cur_total_ballot: U128, + /// current locking token amount (include those expired but hasn't unlock by user) + pub cur_lock_amount: U128, + /// the availabe proposal id for new proposal + pub last_proposal_id: u32, + /// lock near amount for endorsement a proposal + pub lock_amount_per_proposal: U128, + /// current account number in contract + pub account_number: u64, + /// a list of [Relative, Absolute] in which each item is formated as + /// [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}] + pub vote_policy: Vec, + /// in format as {"numerator": n, "denominator": m} + pub nonsense_threshold: Rational, +} +``` + +#### **To view proposal info:** + +```bash +# returns `ProposalInfo` structure or null +near view $REFERENDUM get_proposal_info '{"proposal_id": 0}' + +# returns array of `ProposalInfo` +near view $REFERENDUM get_proposals_in_session '{"session_id": 0}' + +# returns array of proposal id +near view $REFERENDUM get_proposal_ids_in_session '{"session_id": 0}' +``` +The `ProposalInfo` structure is: +```rust +pub struct ProposalInfo{ + pub id: u32, + pub proposer: AccountId, + /// near amount for endorsement + pub lock_amount: U128, + pub description: String, + /// one of the following: + /// "VotePolicy": {"Relative": [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}]} + /// "VotePolicy": {"Absolute": [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}]} + pub vote_policy: proposals::VotePolicy, + /// currently would only be "Vote" + pub kind: proposals::ProposalKind, + /// one of the following: + /// "WarmUp", "InProgress", "Approved", "Rejected", "Nonsense", "Expired" + pub status: proposals::ProposalStatus, + /// [Approve_count, Reject_count, Nonsense_count, Total_ballots] + pub vote_counts: [U128; 4], + /// The session this proposal is valid in + pub session_id: u32, + /// the start time = session_begin_time + start_offset + pub start_offset_sec: u32, + /// the proposal max valid period in seconds + pub lasts_sec: u32, +} +``` +#### **To get account info** +For basic account info: +```bash +# return `AccountInfo` or null +near view $REFERENDUM get_account_info '{"account_id": "alice.near"}' +``` +The `AccountInfo` is: +```rust +pub struct AccountInfo { + /// locked token (XREF) amount + pub locking_amount: U128, + /// ballot amount (calculate at call time) + pub ballot_amount: U128, + /// unlock at the begin of this session, meanwhile ballots reset to zero + pub unlocking_session_id: u32, +} +``` +For account votes: +```bash +# return array of `HumanReadableAccountVote` +near view $REFERENDUM get_account_proposals_in_session '{"account_id": "alice.near", "session_id": 0}' +``` +The `HumanReadableAccountVote` is: +```rust +pub struct HumanReadableAccountVote { + pub proposal_id: u32, + pub vote: Vote, + pub amount: U128, +} +``` + +## Development +1. Install `rustup` via [https://rustup.rs/](https://rustup.rs/) +2. Run the following: +```bash +rustup default stable +rustup target add wasm32-unknown-unknown +``` +### Compiling +You can build release version by running script: +``` +./build_release.sh +``` +### Testing +Contract has unit tests as well as simulation tests. All together can be run: +```bash +cargo test -- --nocapture +``` +### Deploying to TestNet +To deploy to TestNet, you can use: +```bash +near dev-deploy ../res/referendum.wasm +``` +This will output on the contract ID it deployed. diff --git a/referendum/build.sh b/referendum/build.sh new file mode 100644 index 0000000..8b170ea --- /dev/null +++ b/referendum/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +rustup target add wasm32-unknown-unknown +RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release +cd .. +cp target/wasm32-unknown-unknown/release/referendum.wasm ./res/referendum_local.wasm +cd - \ No newline at end of file diff --git a/referendum/build_release.sh b/referendum/build_release.sh new file mode 100644 index 0000000..b79859f --- /dev/null +++ b/referendum/build_release.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +rustup target add wasm32-unknown-unknown +RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release +cd .. +cp target/wasm32-unknown-unknown/release/referendum.wasm ./res/referendum_release.wasm +cd - \ No newline at end of file diff --git a/referendum/src/account.rs b/referendum/src/account.rs new file mode 100644 index 0000000..2a272e0 --- /dev/null +++ b/referendum/src/account.rs @@ -0,0 +1,357 @@ +//! Account is information per user about their locking balances and ballots. +//! + +use crate::utils::{ext_self, GAS_FOR_FT_TRANSFER, GAS_FOR_RESOLVE_TRANSFER, NO_DEPOSIT}; +use crate::*; +use near_contract_standards::fungible_token::core_impl::ext_fungible_token; +use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LookupMap; +use near_sdk::json_types::U128; +use near_sdk::{assert_one_yocto, log, AccountId, Balance, Promise, PromiseOrValue, PromiseResult}; + +use crate::proposals::Vote; + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum VAccount { + Current(Account), +} + +impl VAccount { + /// Upgrades from other versions to the currently used version. + pub fn into_current(self) -> Account { + match self { + VAccount::Current(account) => account, + } + } +} + +impl From for VAccount { + fn from(account: Account) -> Self { + VAccount::Current(account) + } +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct AccountVote { + pub vote: Vote, + pub amount: Balance, +} + +/// Account information. +#[derive(BorshSerialize, BorshDeserialize)] +pub struct Account { + /// The amount of base token locked + pub locking_amount: Balance, + /// The amount of ballots the account holds + pub ballot_amount: Balance, + /// unlocking session id, unlocking at the begining of this session + pub unlocking_session_id: u32, + /// Record proposal voting info + pub proposals: LookupMap, +} + +impl Account { + pub(crate) fn add_locking( + &mut self, + locking_amount: Balance, + ballot_amount: Balance, + unlocking_session_id: u32, + ) { + self.locking_amount += locking_amount; + self.ballot_amount += ballot_amount; + self.unlocking_session_id = unlocking_session_id; + } + + pub(crate) fn remove_locking(&mut self) -> Balance { + if self.ballot_amount == 0 { + let amount = self.locking_amount; + self.locking_amount = 0; + amount + } else { + 0 + } + } + + /// return account's current valid ballot + pub(crate) fn sync_ballot(&self, cur_session_id: u32) -> Balance { + if cur_session_id >= self.unlocking_session_id { + 0 + } else { + self.ballot_amount + } + } + + /// update account's valid ballot according to current session id + pub(crate) fn touch(&mut self, cur_session_id: u32) { + self.ballot_amount = self.sync_ballot(cur_session_id); + } +} + +impl Contract { + /// lasts = 0 means APPEND mode, otherwise NEW mode with session numbers to lock, + /// In APPEND mode, user must have valid ballots, + /// In NEW mode, user must have 0 ballots,but can have unlocked tokens + fn internal_lock(&mut self, account_id: &AccountId, locking_amount: Balance, lasts: u32) { + let current_state = self.data(); + let mut account = current_state + .accounts + .get(account_id) + .map(|va| va.into_current()) + .expect("ERR_USER_NOT_REGISTER"); + let current_session_info = current_state.sessions[current_state.cur_session]; + + account.touch(self.get_cur_session_id()); + + let current_session_remaining_days = nano_to_day(SESSION_INTERMAL) + - nano_to_day( + env::block_timestamp() + - current_state.genesis_timestamp + - current_session_info.session_id as u64 * SESSION_INTERMAL, + ); + + let (ballot_amount, unlocking_session_id) = { + if lasts == 0 { + // APPEND mode, verify non-zero ballot + assert!(account.ballot_amount != 0, "ERR_NO_RUNNING_LOCKING"); + ( + calculate_ballots( + current_session_remaining_days, + locking_amount, + account.unlocking_session_id - current_session_info.session_id), + account.unlocking_session_id + ) + } else { + // NEW mode, verify zero ballot + assert_eq!(account.ballot_amount, 0, "ERR_EXIST_RUNNING_LOCKING"); + ( + calculate_ballots( + current_session_remaining_days, + account.locking_amount + locking_amount, + lasts), + current_session_info.session_id + lasts + ) + } + }; + + // locate end_session (array index) + let end_session = (current_state.cur_session + + (unlocking_session_id - 1 - current_session_info.session_id) as usize) + % MAX_SESSIONS; + + // update account + account.add_locking(locking_amount, ballot_amount, unlocking_session_id); + self.data_mut().cur_total_ballot += ballot_amount; + if lasts == 0 { + // auto update user involved proposal votes + if let Some(proposal_ids) = self + .data() + .proposal_ids_in_sessions + .get(current_session_info.session_id as u64) + { + for proposal_id in proposal_ids { + if let Some(mut account_vote) = account.proposals.get(&proposal_id) { + let append_amount = self.internal_append_vote( + proposal_id, + &account_vote.vote, + &ballot_amount, + ); + account_vote.amount += append_amount; + account.proposals.insert(&proposal_id, &account_vote); + } + } + } + log!( + "User {} appends locking with {} token got {} ballots, total {} ballots", + account_id, + locking_amount, + ballot_amount, + account.ballot_amount, + ); + } else { + log!( + "User {} starts new locking with total {} token got {} ballots, unlocking_session_id : {}", + account_id, + account.locking_amount, + ballot_amount, + unlocking_session_id + ); + } + + self.data_mut().sessions[end_session].expire_amount += ballot_amount; + self.data_mut().accounts.insert(account_id, &account.into()); + } + + fn internal_withdraw(&mut self, account_id: &AccountId) -> Balance { + let current_state = self.data(); + let mut account = current_state + .accounts + .get(account_id) + .map(|va| va.into_current()) + .expect("ERR_USER_NOT_REGISTER"); + account.touch(self.get_cur_session_id()); + let amount = account.remove_locking(); + assert!(amount > 0, "ERR_NOTHING_CAN_BE_WITHDRAW"); + self.data_mut().accounts.insert(account_id, &account.into()); + amount + } + + pub(crate) fn internal_register_account(&mut self, account_id: &AccountId) { + self.data_mut().accounts.insert( + account_id, + &Account { + locking_amount: 0, + ballot_amount: 0, + unlocking_session_id: 0, + proposals: LookupMap::new(StorageKeys::AccountProposals { + account_id: account_id.clone(), + }), + } + .into(), + ); + } + + /// user first vote for given proposal + /// return non-zero ballot power + /// panic if following: + /// * user not registered + /// * user has no valid ballots + /// * user already voted + pub(crate) fn internal_account_vote( + &mut self, + account_id: &AccountId, + proposal_id: u32, + vote: &Vote, + ) -> Balance { + let mut account = self + .data() + .accounts + .get(account_id) + .map(|va| va.into_current()) + .expect("ERR_USER_NOT_REGISTER"); + account.touch(self.get_cur_session_id()); + assert!(account.ballot_amount > 0, "ERR_NO_BALLOTS"); + assert!( + !account.proposals.contains_key(&proposal_id), + "ERR_ALREADY_VOTED" + ); + let account_vote = AccountVote { + vote: vote.clone(), + amount: account.ballot_amount, + }; + account.proposals.insert(&proposal_id, &account_vote); + self.data_mut().accounts.insert(account_id, &account.into()); + account_vote.amount + } +} + +#[near_bindgen] +impl Contract { + /// withdraw unlocked token back to the predecessor account. + /// Requirements: + /// * The predecessor account should be registered. + /// * Requires attached deposit of exactly 1 yoctoNEAR. + /// Return: + /// * Promise or Panic if nothing unlocked + #[payable] + pub fn withdraw(&mut self) -> Promise { + assert_one_yocto(); + + let account_id = env::predecessor_account_id(); + self.fresh_sessions(); + let unlocked = self.internal_withdraw(&account_id); + log!("Withdraw {} token back to {}", unlocked, account_id); + + self.data_mut().cur_lock_amount -= unlocked; + ext_fungible_token::ft_transfer( + account_id.clone(), + U128(unlocked), + None, + &self.data().locked_token, + 1, + GAS_FOR_FT_TRANSFER, + ) + .then(ext_self::callback_post_withdraw( + account_id.clone(), + U128(unlocked), + &env::current_account_id(), + NO_DEPOSIT, + GAS_FOR_RESOLVE_TRANSFER, + )) + } + + #[private] + pub fn callback_post_withdraw(&mut self, sender_id: AccountId, amount: U128) { + assert_eq!( + env::promise_results_count(), + 1, + "ERR: expected 1 promise result from withdraw" + ); + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => {} + PromiseResult::Failed => { + // This reverts the changes from withdraw function. + // If account doesn't exit, the token stays in contract. + if self.data().accounts.contains_key(&sender_id) { + let mut account = self + .data() + .accounts + .get(&sender_id) + .map(|va| va.into_current()) + .unwrap(); + account.locking_amount += amount.0; + self.data_mut().cur_lock_amount += amount.0; + self.data_mut().accounts.insert(&sender_id, &account.into()); + + env::log( + format!( + "Account {} withdraw {} token failed and reverted.", + sender_id, amount.0 + ) + .as_bytes(), + ); + } else { + env::log( + format!( + "Account {} has unregisterd. withdraw {} token goes to contract.", + sender_id, amount.0 + ) + .as_bytes(), + ); + } + } + }; + } +} + +#[near_bindgen] +impl FungibleTokenReceiver for Contract { + /// Callback on receiving tokens by this contract. + fn ft_on_transfer( + &mut self, + sender_id: ValidAccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue { + // sync point + self.fresh_sessions(); + + let token_in = env::predecessor_account_id(); + let amount: Balance = amount.into(); + assert_eq!(token_in, self.data().locked_token, "ERR_ILLEGAL_TOKEN"); + + if msg.is_empty() { + // user append locking + self.internal_lock(sender_id.as_ref(), amount, 0); + } else { + // new locking + let locking_period = msg.parse::().expect("ERR_ILLEGAL_MSG"); + assert!(locking_period > 0, "ERR_ILLEGAL_MSG"); + assert!((locking_period as usize) <= MAX_SESSIONS, "ERR_ILLEGAL_MSG"); + self.internal_lock(sender_id.as_ref(), amount, locking_period); + } + self.data_mut().cur_lock_amount += amount; + PromiseOrValue::Value(U128(0)) + } +} diff --git a/referendum/src/lib.rs b/referendum/src/lib.rs new file mode 100644 index 0000000..99235dd --- /dev/null +++ b/referendum/src/lib.rs @@ -0,0 +1,135 @@ +/*! +* REF referendum contract +* +*/ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{LookupMap, Vector}; +use near_sdk::json_types::ValidAccountId; +use near_sdk::{env, near_bindgen, AccountId, Balance, BorshStorageKey, PanicOnDefault, Timestamp}; +use proposals::VotePolicy; + +use crate::account::VAccount; +use crate::proposals::VersionedProposal; +use crate::session::SessionInfo; +use crate::utils::*; + +mod account; +mod owner; +mod proposals; +mod session; +mod storage_impl; +mod utils; +mod views; + +near_sdk::setup_alloc!(); + +#[derive(BorshStorageKey, BorshSerialize)] +pub enum StorageKeys { + Accounts, + Proposals, + ProposalIdsInSession, + AccountProposals { account_id: AccountId }, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct ContractData { + // owner of this contract + owner_id: AccountId, + + // which token used for locking + locked_token: AccountId, + + // the genesis timestamp + genesis_timestamp: Timestamp, + + // maintains a global session circle array + sessions: [SessionInfo; MAX_SESSIONS], + + // each session contains proposal id array + proposal_ids_in_sessions: Vector>, + + // current session idx in sessions array + cur_session: usize, + + // total ballot amount in current session + cur_total_ballot: Balance, + // total lock token amount + cur_lock_amount: Balance, + + accounts: LookupMap, + account_number: u64, + + // the global vote policy + vote_policy: Vec, + + /// Last available id for the proposals. + pub last_proposal_id: u32, + /// Proposal map from ID to proposal information. + pub proposals: LookupMap, + + /// limits + pub lock_amount_per_proposal: Balance, + pub nonsense_threshold: Rational, +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub enum VContractData { + Current(ContractData), +} + +impl VContractData {} + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct Contract { + data: VContractData, +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new(owner_id: ValidAccountId, token_id: ValidAccountId) -> Self { + assert!(!env::state_exists(), "Already initialized"); + Self { + data: VContractData::Current(ContractData { + owner_id: owner_id.into(), + locked_token: token_id.into(), + genesis_timestamp: env::block_timestamp() + DEFAULT_GENESIS_OFFSET, + sessions: [SessionInfo::default(); MAX_SESSIONS], + proposal_ids_in_sessions: Vector::new(StorageKeys::ProposalIdsInSession), + cur_session: 0, + cur_total_ballot: 0, + cur_lock_amount: 0, + accounts: LookupMap::new(StorageKeys::Accounts), + account_number: 0, + vote_policy: vec![DEFAULT_VP_RELATIVE, DEFAULT_VP_ABSOLUTE], + last_proposal_id: 0, + proposals: LookupMap::new(StorageKeys::Proposals), + lock_amount_per_proposal: DEFAULT_LOCK_NEAR_AMOUNT_FOR_PROPOSAL, + nonsense_threshold: DEFAULT_NONSENSE_THRESHOLD, + }), + } + } +} + +impl Contract { + fn data(&self) -> &ContractData { + match &self.data { + VContractData::Current(data) => data, + } + } + + fn data_mut(&mut self) -> &mut ContractData { + match &mut self.data { + VContractData::Current(data) => data, + } + } + + fn has_launch(&self) -> bool { + return env::block_timestamp() > self.data().genesis_timestamp; + } + + fn get_cur_session_id(&self) -> u32 { + ((env::block_timestamp() - self.data().genesis_timestamp) / SESSION_INTERMAL) as u32 + } +} diff --git a/referendum/src/owner.rs b/referendum/src/owner.rs new file mode 100644 index 0000000..dc40619 --- /dev/null +++ b/referendum/src/owner.rs @@ -0,0 +1,142 @@ +//! Implement all the relevant logic for owner of this contract. + +use near_sdk::json_types::U128; +use crate::*; + + +#[near_bindgen] +impl Contract { + /// Change owner. Only can be called by owner. + pub fn set_owner(&mut self, owner_id: ValidAccountId) { + self.assert_owner(); + self.data_mut().owner_id = owner_id.as_ref().clone(); + } + + /// Get the owner of this account. + pub fn get_owner(&self) -> AccountId { + self.data().owner_id.clone() + } + + pub fn modify_genesis_timestamp(&mut self, genesis_timestamp_in_sec: u32) { + self.assert_owner(); + let genesis_ts = sec_to_nano(genesis_timestamp_in_sec); + assert!( + env::block_timestamp() <= self.data().genesis_timestamp, + "ERR_HAS_LAUNCHED" + ); + assert!(genesis_ts > env::block_timestamp(), "ERR_ILLEGAL_GENESIS_TIME"); + self.data_mut().genesis_timestamp = genesis_ts; + } + + pub fn modify_endorsement_amount(&mut self, amount: U128) { + self.assert_owner(); + let amount: Balance = amount.into(); + assert!(amount > 0, "ERR_MUST_HAVE_ENDORSEMENT"); + self.data_mut().lock_amount_per_proposal = amount; + } + + pub fn modify_nonsense_threshold(&mut self, threshold: Rational) { + self.assert_owner(); + assert!(threshold.is_valid(), "ERR_ILLEGAL_THRESHOLD"); + self.data_mut().nonsense_threshold = threshold; + } + + pub fn modify_vote_policy(&mut self, vote_policy: VotePolicy) { + self.assert_owner(); + match &vote_policy { + VotePolicy::Relative(l, j) => { + assert!(l.is_valid(), "ERR_ILLEGAL_THRESHOLD"); + assert!(j.is_valid(), "ERR_ILLEGAL_THRESHOLD"); + if let Some(elem) = self.data_mut().vote_policy.get_mut(0) { + *elem = vote_policy; + } + }, + VotePolicy::Absolute(p, f) => { + assert!(p.is_valid(), "ERR_ILLEGAL_THRESHOLD"); + assert!(f.is_valid(), "ERR_ILLEGAL_THRESHOLD"); + if let Some(elem) = self.data_mut().vote_policy.get_mut(1) { + *elem = vote_policy; + } + }, + } + } + + pub(crate) fn assert_owner(&self) { + assert_eq!( + env::predecessor_account_id(), + self.data().owner_id, + "ERR_NOT_ALLOWED" + ); + } + + pub(crate) fn assert_launch(&self) { + assert!(self.has_launch(), "ERR_NOT_LAUNCHED"); + } + + /// Migration function. + /// For next version upgrades, change this function. + #[init(ignore_state)] + #[private] + pub fn migrate() -> Self { + let prev: Contract = env::state_read().expect("ERR_NOT_INITIALIZED"); + prev + } +} + + +#[cfg(target_arch = "wasm32")] +mod upgrade { + use near_sdk::env::BLOCKCHAIN_INTERFACE; + use near_sdk::Gas; + + use super::*; + + const BLOCKCHAIN_INTERFACE_NOT_SET_ERR: &str = "Blockchain interface not set."; + + /// Gas for calling migration call. + pub const GAS_FOR_MIGRATE_CALL: Gas = 5_000_000_000_000; + + /// Self upgrade and call migrate, optimizes gas by not loading into memory the code. + /// Takes as input non serialized set of bytes of the code. + #[no_mangle] + pub extern "C" fn upgrade() { + env::setup_panic_hook(); + env::set_blockchain_interface(Box::new(near_blockchain::NearBlockchain {})); + let contract: Contract = env::state_read().expect("ERR_CONTRACT_IS_NOT_INITIALIZED"); + contract.assert_owner(); + let current_id = env::current_account_id().into_bytes(); + let method_name = "migrate".as_bytes().to_vec(); + unsafe { + BLOCKCHAIN_INTERFACE.with(|b| { + // Load input into register 0. + b.borrow() + .as_ref() + .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) + .input(0); + 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 _); + b.borrow() + .as_ref() + .expect(BLOCKCHAIN_INTERFACE_NOT_SET_ERR) + .promise_batch_action_deploy_contract(promise_id, u64::MAX as _, 0); + let attached_gas = env::prepaid_gas() - env::used_gas() - GAS_FOR_MIGRATE_CALL; + 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, + ); + }); + } + } + +} diff --git a/referendum/src/proposals.rs b/referendum/src/proposals.rs new file mode 100644 index 0000000..ea3c346 --- /dev/null +++ b/referendum/src/proposals.rs @@ -0,0 +1,479 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{log, AccountId, Balance, Promise, Timestamp}; +use near_sdk::json_types::U128; + +use crate::utils::Rational; +use crate::*; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(crate = "near_sdk::serde")] +pub enum PolicyType { + Relative = 0x0, + Absolute = 0x1, +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for PolicyType { + fn from(tp: u8) -> Self { + match tp { + 0 => PolicyType::Relative, + 1 => PolicyType::Absolute, + _ => env::panic(b"ERR_INVALID_POLICY_TYPE"), + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +#[serde(crate = "near_sdk::serde")] +pub enum VotePolicy { + Relative(Rational, Rational), + Absolute(Rational, Rational), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From> for VotePolicy { + fn from(content: Vec) -> Self { + VotePolicy::try_from_slice(&content).unwrap() + } +} + +impl VotePolicy { + /// to see if the proposal goes to a final state + pub fn judge( + &self, + approve_power: &Balance, + reject_power: &Balance, + nonsense_power: &Balance, + total: &Balance, + nonsense_threshold: &Rational, + ) -> ProposalStatus { + if nonsense_threshold.pass(nonsense_power, total) { + ProposalStatus::Nonsense + } else { + match self { + VotePolicy::Relative(limit, threshold) => { + let voted = approve_power + reject_power + nonsense_power; + if limit.pass(&voted, total) { + if threshold.pass(reject_power, &voted) { + ProposalStatus::Rejected + } else if threshold.pass(approve_power, &voted) { + ProposalStatus::Approved + } else { + ProposalStatus::InProgress + } + } else { + ProposalStatus::InProgress + } + } + VotePolicy::Absolute(pass_threshold, fail_threshold) => { + if fail_threshold.pass(reject_power, total) { + ProposalStatus::Rejected + } else if pass_threshold.pass(approve_power, total) { + ProposalStatus::Approved + } else { + ProposalStatus::InProgress + } + } + } + } + } + + pub fn is_valid(&self) -> bool { + match self { + VotePolicy::Relative(limit, threshold) => limit.is_valid() && threshold.is_valid(), + VotePolicy::Absolute(pass_threshold, fail_threshold) => { + pass_threshold.is_valid() && fail_threshold.is_valid() + } + } + } +} + +/// Status of a proposal. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone, PartialEq, Debug)] +#[serde(crate = "near_sdk::serde")] +pub enum ProposalStatus { + WarmUp, + 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 nonsense (e.g. spam), this proposal is rejected and bond is not returned. + /// Interfaces shouldn't show nonsense proposals. + Nonsense, + /// Expired after period of time. + Expired, +} + +/// Kinds of proposals, doing different action. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +#[serde(crate = "near_sdk::serde")] +pub enum ProposalKind { + /// Just a single vote, with no execution. + Vote, +} + +impl From<&str> for ProposalKind { + fn from(kind: &str) -> Self { + match kind { + "vote" => ProposalKind::Vote, + _ => env::panic(b"ERR_INVALID_PROPOSAL_KIND"), + } + } +} + +/// Set of possible action to take. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub enum Action { + /// Vote to approve given proposal + VoteApprove, + /// Vote to reject given proposal + VoteReject, + /// Vote to nonsense given proposal(because it's spam). + VoteNonsense, +} + +impl From<&str> for Action { + fn from(action: &str) -> Self { + match action { + "approve" => Action::VoteApprove, + "reject" => Action::VoteReject, + "nonsense" => Action::VoteNonsense, + _ => env::panic(b"ERR_INVALID_ACTION_KIND"), + } + } +} + +/// Votes recorded in the proposal. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub enum Vote { + Approve = 0x0, + Reject = 0x1, + Nonsense = 0x2, +} + +impl From for Vote { + fn from(action: Action) -> Self { + match action { + Action::VoteApprove => Vote::Approve, + Action::VoteReject => Vote::Reject, + Action::VoteNonsense => Vote::Nonsense, + } + } +} + +/// 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, + /// The locked near as the endorsement of this proposal + pub lock_amount: Balance, + /// Description of this proposal. + pub description: String, + /// Voting rule details + pub vote_policy: VotePolicy, + /// Kind of proposal with relevant information. + pub kind: ProposalKind, + /// Current status of the proposal. + pub status: ProposalStatus, + /// Count of votes per role per opinion and total: Approve / Reject / Nonsense / Total. + pub vote_counts: [Balance; 4], + /// Session ID for voting period. + pub session_id: u32, + /// the nano seconds of voting begin time after the session begin for the proposal, + /// before this time, proposer can remove this immediately. + pub start_offset: Timestamp, + /// the nano seconds of voting lasts after start_offset for the proposal, + /// An inprogress poposal would change to expired after it. + /// The (start_offset+lasts) should less than SESSION_INTERVAL. + pub lasts: Timestamp, +} + +#[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 votes to proposal. + /// pre-requisite: status == InProgress + pub fn update_votes( + &mut self, + vote: &Vote, + amount: &Balance, + total: &Balance, + nonsense_threshold: &Rational, + ) { + self.vote_counts[vote.clone() as usize] += amount; + self.vote_counts[3] = total.clone(); + + self.status = self.vote_policy.judge( + &self.vote_counts[0], + &self.vote_counts[1], + &self.vote_counts[2], + &self.vote_counts[3], + nonsense_threshold, + ); + } + + pub fn get_cur_status(&self, genesis_ts: Timestamp) -> ProposalStatus { + let cur_ts = env::block_timestamp(); + let session_start = genesis_ts + self.session_id as u64 * SESSION_INTERMAL; + let begin_ts = session_start + self.start_offset; + let end_ts = begin_ts + self.lasts; + match self.status { + ProposalStatus::WarmUp => { + if cur_ts > end_ts { + ProposalStatus::Expired + } else if cur_ts > begin_ts { + ProposalStatus::InProgress + } else { + self.status.clone() + } + } + ProposalStatus::InProgress => { + if cur_ts > end_ts { + ProposalStatus::Expired + } else { + self.status.clone() + } + } + _ => self.status.clone(), + } + } +} + +impl Contract { + pub(crate) fn internal_append_vote( + &mut self, + id: u32, + vote: &Vote, + amount: &Balance, + ) -> Balance { + let mut proposal: Proposal = self + .data() + .proposals + .get(&id) + .expect("ERR_NO_PROPOSAL") + .into(); + let cur_status = proposal.get_cur_status(self.data().genesis_timestamp); + proposal.status = cur_status; + + // check proposal is inprogress + match proposal.status { + ProposalStatus::InProgress => { + // update and judge proposal result + proposal.update_votes( + &vote, + amount, + &self.data().cur_total_ballot, + &self.data().nonsense_threshold, + ); + if proposal.status == ProposalStatus::Approved + || proposal.status == ProposalStatus::Rejected + { + // return lock near to proposer + Promise::new(proposal.proposer.clone()).transfer(proposal.lock_amount); + proposal.lock_amount = 0; + } + self.data_mut() + .proposals + .insert(&id, &VersionedProposal::Default(proposal)); + *amount + }, + _ => 0, + } + } +} + +#[near_bindgen] +impl Contract { + /// Add proposal to this DAO. + #[payable] + pub fn add_proposal( + &mut self, + description: String, + kind: ProposalKind, + policy_type: PolicyType, + session_id: u32, + start_offset_sec: u32, + lasts_sec: u32, + ) -> u32 { + // check point + self.fresh_sessions(); + + let proposer = env::predecessor_account_id(); + + // check and lock deposit + let deposit_amount = env::attached_deposit(); + assert!( + deposit_amount >= self.data().lock_amount_per_proposal, + "ERR_NOT_ENOUGH_LOCK_NEAR" + ); + if deposit_amount > self.data().lock_amount_per_proposal { + Promise::new(proposer.clone()) + .transfer(deposit_amount - self.data().lock_amount_per_proposal); + } + + // Check time validation, session_id gte cur_session_id, (session_id.begin_ts+start_offset+lasts) lt (session_id+1).begin_ts + let current_session_id = self.data().sessions[self.data().cur_session].session_id; + assert!( + session_id >= current_session_id, + "ERR_SESSION_ID_NEED_GE_CURRENT_SESSION_ID" + ); + let base_timestamp = self.data().genesis_timestamp + SESSION_INTERMAL * session_id as u64; + assert!( + (base_timestamp + sec_to_nano(start_offset_sec)) > env::block_timestamp(), + "ERR_PROPOSAL_START_TIME_NEED_GE_CURRENT_TIME" + ); + assert!( + (base_timestamp + sec_to_nano(start_offset_sec + lasts_sec)) + < base_timestamp + SESSION_INTERMAL, + "ERR_PROPOSAL_END_TIME_NEED_LE_NEXT_SESSION_BEGIN_TIME" + ); + + let ps = Proposal { + proposer, + lock_amount: self.data().lock_amount_per_proposal, + description, + vote_policy: self + .data() + .vote_policy + .get(policy_type as usize) + .unwrap() + .clone(), + kind, + status: ProposalStatus::WarmUp, + vote_counts: [0; 4], + session_id, + start_offset: sec_to_nano(start_offset_sec), + lasts: sec_to_nano(lasts_sec), + }; + + // actually add proposal to this DAO + let id = self.data().last_proposal_id; + self.data_mut() + .proposals + .insert(&id, &VersionedProposal::Default(ps)); + self.data_mut().last_proposal_id += 1; + + self.add_proposal_to_session(id, session_id); + + id + } + + /// proposer can call this to remove proposal before start time. + /// id: proposal id + /// return true if sucessfully removed, false if already start + /// panic if following: + /// * no proposal found + /// * predecessor not prposer + pub fn remove_proposal(&mut self, id: u32) -> bool { + // sync point + self.fresh_sessions(); + let proposal: Proposal = self + .data() + .proposals + .get(&id) + .expect("ERR_NO_PROPOSAL") + .into(); + assert_eq!( + proposal.proposer, + env::predecessor_account_id(), + "ERR_NOT_ALLOW" + ); + let cur_status = proposal.get_cur_status(self.data().genesis_timestamp); + match cur_status { + ProposalStatus::WarmUp => { + if proposal.lock_amount > 0 { + Promise::new(proposal.proposer.clone()).transfer(proposal.lock_amount); + } + self.data_mut().proposals.remove(&id); + + self.remove_proposal_from_session(id, proposal.session_id); + + true + } + _ => false, + } + } + + /// When a proposal expired, the proposer can call this to redeem locked near + /// id: proposal id + /// return true if schedule to transfer back locked near, false if nothing to redeem (already redeemed or nonsense) + /// panic if following: + /// * no proposal found + /// * predecessor not prposer + pub fn redeem_near_in_expired_proposal(&mut self, id: u32) -> bool { + // sync point + self.fresh_sessions(); + let mut proposal: Proposal = self + .data() + .proposals + .get(&id) + .expect("ERR_NO_PROPOSAL") + .into(); + assert_eq!( + proposal.proposer, + env::predecessor_account_id(), + "ERR_NOT_ALLOW" + ); + let cur_status = proposal.get_cur_status(self.data().genesis_timestamp); + proposal.status = cur_status; + if proposal.lock_amount > 0 && proposal.status == ProposalStatus::Expired { + Promise::new(proposal.proposer.clone()).transfer(proposal.lock_amount); + proposal.lock_amount = 0; + self.data_mut() + .proposals + .insert(&id, &VersionedProposal::Default(proposal)); + true + } else { + false + } + } + + /// Act on given proposal by id, if permissions allow. + /// id: propoal id + /// action: one of "VoteApprove", "VoteReject", "VoteNonsense" + /// memo: is logged but not stored in the state. Can be used to leave notes or explain the action. + /// return accepted ballot power + /// would panic if act failed + pub fn act_proposal(&mut self, id: u32, action: Action, memo: Option) -> U128 { + // sync point + self.fresh_sessions(); + + let account_id = env::predecessor_account_id(); + + let vote: Vote = action.into(); + let ballot_amount = self.internal_account_vote(&account_id, id, &vote); + + let accept_ballot = self.internal_append_vote(id, &vote, &ballot_amount); + assert_eq!(accept_ballot, ballot_amount, "ERR_PROPOSAL_NOT_VOTABLE"); + + if let Some(memo) = memo { + log!("Memo: {}", memo); + } + + accept_ballot.into() + } +} diff --git a/referendum/src/session.rs b/referendum/src/session.rs new file mode 100644 index 0000000..2b1267a --- /dev/null +++ b/referendum/src/session.rs @@ -0,0 +1,101 @@ +//! Session stores information per session + +use crate::*; + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::Balance; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Copy, Default)] +pub struct SessionInfo { + pub session_id: u32, + pub expire_amount: Balance, +} + +impl Contract { + /// get newest cur ballots + pub(crate) fn calc_cur_ballots(&self) -> Balance { + if self.has_launch() { + let cur_session_id = self.get_cur_session_id(); + if self.data().sessions[1].session_id == 0 { + // hasn't initialized + 0 + } else { + // get real ballot + let head = self.data().cur_session; + let mut ballot = self.data().cur_total_ballot; + for i in 0..MAX_SESSIONS { + let idx = (i + head) % MAX_SESSIONS; + let session = self.data().sessions[idx].clone(); + if session.session_id < cur_session_id { + // expire ballot + ballot -= session.expire_amount; + } else { + break; + } + } + ballot + } + } else { + // before launch, ballot amount is 0 + 0 + } + } + + /// update sessions. + pub(crate) fn fresh_sessions(&mut self) { + self.assert_launch(); + let cur_session_id = self.get_cur_session_id(); + + let head = self.data().cur_session; + if self.data().sessions[1].session_id == 0 { + // initialize session info + for i in 0..MAX_SESSIONS { + self.data_mut().sessions[i].session_id = cur_session_id + i as u32; + self.data_mut().proposal_ids_in_sessions.push(&vec![]); + } + } else { + // checkpoint logic + for i in 0..MAX_SESSIONS { + let idx = (i + head) % MAX_SESSIONS; + let session = self.data().sessions[idx].clone(); + if session.session_id < cur_session_id { + // expire ballot + self.data_mut().cur_total_ballot -= session.expire_amount; + // prepare for new session + self.data_mut().sessions[idx].expire_amount = 0; + self.data_mut().sessions[idx].session_id = + session.session_id + MAX_SESSIONS as u32; + self.data_mut().proposal_ids_in_sessions.push(&vec![]); + } else { + // spin to the new head + self.data_mut().cur_session = idx; + break; + } + } + } + } + + pub(crate) fn add_proposal_to_session(&mut self, proposal_id: u32, session_id: u32) { + let mut proposal_ids = self + .data() + .proposal_ids_in_sessions + .get(session_id as u64) + .unwrap(); + proposal_ids.push(proposal_id); + self.data_mut() + .proposal_ids_in_sessions + .replace(session_id as u64, &proposal_ids); + } + + pub(crate) fn remove_proposal_from_session(&mut self, proposal_id: u32, session_id: u32) { + let mut proposal_ids = self + .data() + .proposal_ids_in_sessions + .get(session_id as u64) + .unwrap(); + proposal_ids.retain(|&x| x != proposal_id); + self.data_mut() + .proposal_ids_in_sessions + .replace(session_id as u64, &proposal_ids); + } +} diff --git a/referendum/src/storage_impl.rs b/referendum/src/storage_impl.rs new file mode 100644 index 0000000..169a68a --- /dev/null +++ b/referendum/src/storage_impl.rs @@ -0,0 +1,91 @@ +use crate::*; +use near_contract_standards::storage_management::{ + StorageBalance, StorageBalanceBounds, StorageManagement, +}; + +use std::convert::TryInto; +use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::{assert_one_yocto, env, near_bindgen, Promise}; + +#[near_bindgen] +impl StorageManagement for Contract { + #[allow(unused_variables)] + #[payable] + fn storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + self.assert_launch(); + let amount = env::attached_deposit(); + let account_id = account_id + .map(|a| a.into()) + .unwrap_or_else(|| env::predecessor_account_id()); + let min_balance = self.storage_balance_bounds().min.0; + let already_registered = self.data().accounts.contains_key(&account_id); + if amount < min_balance && !already_registered { + env::panic(b"ERR_DEPOSIT_LESS_THAN_MIN_STORAGE"); + } + if already_registered { + if amount > 0 { + Promise::new(env::predecessor_account_id()).transfer(amount); + } + } else { + self.internal_register_account(&account_id); + self.data_mut().account_number += 1; + let refund = amount - min_balance; + if refund > 0 { + Promise::new(env::predecessor_account_id()).transfer(refund); + } + } + self.storage_balance_of(account_id.try_into().unwrap()) + .unwrap() + } + + #[allow(unused_variables)] + #[payable] + fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + assert_one_yocto(); + env::panic(b"ERR_NO_STORAGE_CAN_WITHDRAW"); + } + + #[allow(unused_variables)] + #[payable] + fn storage_unregister(&mut self, force: Option) -> bool { + assert_one_yocto(); + self.assert_launch(); + let current_state = self.data(); + let account_id = env::predecessor_account_id(); + if let Some(VAccount::Current(account)) = current_state.accounts.get(&account_id) { + assert!( + account.locking_amount == 0, + "ERR_ACCOUNT_NOT_UNLOCK" + ); + self.data_mut().accounts.remove(&account_id); + let number = self.data().account_number.checked_sub(1).unwrap_or(0); + self.data_mut().account_number = number; + Promise::new(account_id.clone()).transfer(STORAGE_BALANCE_MIN_BOUND); + true + } else { + false + } + } + + fn storage_balance_bounds(&self) -> StorageBalanceBounds { + StorageBalanceBounds { + min: U128(STORAGE_BALANCE_MIN_BOUND), + max: None, + } + } + + fn storage_balance_of(&self, account_id: ValidAccountId) -> Option { + if self.data().accounts.contains_key(&account_id.into()) { + Some(StorageBalance { + total: U128(STORAGE_BALANCE_MIN_BOUND), + available: U128(0), + }) + }else{ + None + } + } +} \ No newline at end of file diff --git a/referendum/src/utils.rs b/referendum/src/utils.rs new file mode 100644 index 0000000..2a34544 --- /dev/null +++ b/referendum/src/utils.rs @@ -0,0 +1,106 @@ +//! Utils stores pub info + +use std::ops::Mul; + +use near_sdk::json_types::U128; +use near_sdk::{ext_contract, Gas, Timestamp, Balance}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use uint::construct_uint; + +construct_uint! { + /// 256-bit unsigned integer. + pub struct U256(4); +} +use crate::proposals::VotePolicy; + +/// Attach no deposit. +pub const NO_DEPOSIT: u128 = 0; + +pub const GAS_FOR_RESOLVE_TRANSFER: Gas = 10_000_000_000_000; + +pub const GAS_FOR_FT_TRANSFER: Gas = 20_000_000_000_000; + +/// meanwhile the max locking period +pub const MAX_SESSIONS: usize = 24; + +/// each session lasts 30 days +pub const SESSION_INTERMAL: u64 = 3600 * 24 * 30 * 1_000_000_000; + +/// make the default launch time to be 30 days after contract initiation +pub const DEFAULT_GENESIS_OFFSET: u64 = 3600 * 24 * 30 * 1_000_000_000; + +pub const STORAGE_BALANCE_MIN_BOUND: u128 = 10_000_000_000_000_000_000_000; +/// default locking amount is 10 near for each proposal +pub const DEFAULT_LOCK_NEAR_AMOUNT_FOR_PROPOSAL: Balance = 10_000_000_000_000_000_000_000_000; + +pub const DEFAULT_NONSENSE_THRESHOLD: Rational = Rational {numerator: 1, denominator: 2}; + +pub const DEFAULT_VP_RELATIVE: VotePolicy = VotePolicy::Relative( + Rational {numerator: 33, denominator: 100}, + Rational {numerator: 1, denominator: 2} +); + +pub const DEFAULT_VP_ABSOLUTE: VotePolicy = VotePolicy::Absolute( + Rational {numerator: 45, denominator: 100}, + Rational {numerator: 33, denominator: 100} +); + + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[derive(BorshDeserialize, BorshSerialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub struct Rational { + numerator: u32, + denominator: u32, +} + +impl From> for Rational { + fn from(content: Vec) -> Self { + Rational::try_from_slice(&content).unwrap() + } +} + +impl Rational { + + pub fn pass(&self, num: &Balance, denom: &Balance) -> bool { + U256::from(*num).mul(U256::from(self.denominator)). + ge(&U256::from(self.numerator).mul(U256::from(*denom))) + } + + pub fn is_valid(&self) -> bool { + self.numerator > 0 && self.denominator >= self.numerator + } +} + +pub fn nano_to_sec(nano: Timestamp) -> u32 { + (nano / 1_000_000_000) as u32 +} + +pub fn sec_to_nano(sec: u32) -> Timestamp { + sec as u64 * 1_000_000_000 +} + +#[ext_contract(ext_self)] +pub trait Withdraw { + fn callback_post_withdraw( + &mut self, + sender_id: AccountId, + amount: U128, + ); +} + +pub fn nano_to_day(nano: Timestamp) -> u64 { + nano / (3600 * 24 * 1_000_000_000) +} + +pub fn calculate_ballots(current_session_remaining_days: u64, total_amount: Balance, locking_period: u32) -> Balance{ + assert!(locking_period > 0, "ERR_ILLEGAL_LASTS"); + let future_session_ballots = total_amount * (locking_period - 1) as u128; + let current_session_ballots = (U256::from(total_amount) + * U256::from(current_session_remaining_days) + / U256::from(nano_to_day(SESSION_INTERMAL))) + .as_u128(); + future_session_ballots + current_session_ballots +} \ No newline at end of file diff --git a/referendum/src/views.rs b/referendum/src/views.rs new file mode 100644 index 0000000..9753884 --- /dev/null +++ b/referendum/src/views.rs @@ -0,0 +1,208 @@ +//! View functions for the contract. + +use crate::*; +use crate::proposals::Vote; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::json_types::U128; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub struct ContractMetadata { + /// the owner account id of contract + pub owner_id: AccountId, + /// accept lock token account id + pub locked_token: AccountId, + /// the launch timestamp in seconds + pub genesis_timestamp_sec: u32, + /// current session id (start from 0) + pub cur_session_id: u32, + /// current total ballot amount (calculate at call time) + pub cur_total_ballot: U128, + /// current locking token amount (include those expired but hasn't unlock by user) + pub cur_lock_amount: U128, + /// the availabe proposal id for new proposal + pub last_proposal_id: u32, + /// lock near amount for endorsement a proposal + pub lock_amount_per_proposal: U128, + /// current account number in contract + pub account_number: u64, + /// a list of [Relative, Absolute] in which each item is formated as + /// [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}] + pub vote_policy: Vec, + /// in format as {"numerator": n, "denominator": m} + pub nonsense_threshold: Rational, +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub struct ProposalInfo{ + pub id: u32, + pub proposer: AccountId, + /// near amount for endorsement + pub lock_amount: U128, + pub description: String, + /// one of the following: + /// "VotePolicy": {"Relative": [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}]} + /// "VotePolicy": {"Absolute": [{"numerator": n, "denominator": m}, {"numerator": n, "denominator": m}]} + pub vote_policy: proposals::VotePolicy, + /// currently would only be "Vote" + pub kind: proposals::ProposalKind, + /// one of the following: + /// "WarmUp", "InProgress", "Approved", "Rejected", "Nonsense", "Expired" + pub status: proposals::ProposalStatus, + /// [Approve_count, Reject_count, Nonsense_count, Total_ballots] + pub vote_counts: [U128; 4], + /// The session this proposal is valid in + pub session_id: u32, + /// the start time = session_begin_time + start_offset + pub start_offset_sec: u32, + /// the proposal max valid period in seconds + pub lasts_sec: u32, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub struct AccountInfo { + /// locked token (XREF) amount + pub locking_amount: U128, + /// ballot amount (calculate at call time) + pub ballot_amount: U128, + /// unlock at the begin of this session, meanwhile ballots reset to zero + pub unlocking_session_id: u32, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +pub struct HumanReadableAccountVote { + pub proposal_id: u32, + pub vote: Vote, + pub amount: U128, +} + + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq, Clone, Copy))] +pub struct SessionState { + pub session_id: u32, + pub expire_amount: U128, +} + +impl From for SessionState { + fn from(session_info: SessionInfo) -> Self { + Self { + session_id: session_info.session_id, + expire_amount: session_info.expire_amount.into(), + } + } +} + +#[near_bindgen] +impl Contract { + /// Return contract basic info + pub fn contract_metadata(&self) -> ContractMetadata { + let current_state = self.data(); + ContractMetadata { + owner_id: current_state.owner_id.clone(), + locked_token: current_state.locked_token.clone(), + genesis_timestamp_sec: nano_to_sec(current_state.genesis_timestamp), + cur_session_id: self.get_cur_session_id(), + cur_total_ballot: self.calc_cur_ballots().into(), + cur_lock_amount: current_state.cur_lock_amount.into(), + last_proposal_id: current_state.last_proposal_id, + lock_amount_per_proposal: U128(current_state.lock_amount_per_proposal), + account_number: current_state.account_number, + vote_policy: current_state.vote_policy.clone(), + nonsense_threshold: current_state.nonsense_threshold.clone(), + } + } + + /// get single proposal current info + pub fn get_proposal_info(&self, proposal_id: u32) -> Option{ + if let Some(VersionedProposal::Default(proposal)) = self.data().proposals.get(&proposal_id).as_ref() { + Some(ProposalInfo{ + id: proposal_id, + proposer: proposal.proposer.clone(), + lock_amount: U128(proposal.lock_amount), + description: proposal.description.clone(), + vote_policy: proposal.vote_policy.clone(), + kind: proposal.kind.clone(), + status: proposal.get_cur_status(self.data().genesis_timestamp), + vote_counts: proposal.vote_counts.map(|v| U128(v)), + session_id: proposal.session_id, + start_offset_sec: nano_to_sec(proposal.start_offset), + lasts_sec: nano_to_sec(proposal.lasts), + }) + }else{ + None + } + } + + /// get proposals by session + pub fn get_proposals_in_session(&self, session_id: u32) -> Vec { + let mut ret: Vec = vec![]; + for id in self.get_proposal_ids_in_session(session_id) { + if let Some(item) = self.get_proposal_info(id) { + ret.push(item); + } + } + ret + } + + pub fn get_proposal_ids_in_session(&self, session_id: u32) -> Vec { + match self.data().proposal_ids_in_sessions.get(session_id as u64) { + Some(proposal_ids) => proposal_ids, + None => vec![] + } + } + + pub fn get_account_info(&self, account_id: ValidAccountId) -> Option { + if let Some(vacc) = self.data().accounts.get(account_id.as_ref()) { + match vacc { + VAccount::Current(acc) => { + Some(AccountInfo { + locking_amount: acc.locking_amount.into(), + ballot_amount: acc.sync_ballot(self.get_cur_session_id()).into(), + unlocking_session_id: acc.unlocking_session_id, + }) + } + } + } else { + None + } + } + + pub fn get_account_proposals_in_session(&self, account_id: ValidAccountId, session_id: u32) -> Vec { + let mut ret: Vec = vec![]; + match self.data().proposal_ids_in_sessions.get(session_id as u64) { + Some(proposal_ids) => { + if let Some(vacc) = self.data().accounts.get(account_id.as_ref()) { + match vacc { + VAccount::Current(acc) => { + for proposal_id in proposal_ids { + if let Some(account_vote) = acc.proposals.get(&proposal_id).as_ref() { + ret.push(HumanReadableAccountVote { + proposal_id, + vote: account_vote.vote.clone(), + amount: account_vote.amount.into(), + }); + } + } + } + } + } + }, + None => {}, + } + ret + } + + // TODO: maybe unnecessary to get session info + pub fn get_session_state(&self, session_idx: usize) -> SessionState { + self.data().sessions[session_idx].into() + } +} \ No newline at end of file diff --git a/referendum/tests/common/init.rs b/referendum/tests/common/init.rs new file mode 100644 index 0000000..02e03f8 --- /dev/null +++ b/referendum/tests/common/init.rs @@ -0,0 +1,74 @@ +#![allow(unused)] +use near_sdk_sim::{call, deploy, init_simulator, to_yocto, ContractAccount, UserAccount}; +use test_token::ContractContract as TestToken; +use referendum::ContractContract as Referendum; +use crate::*; + +near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { + TEST_WASM_BYTES => "../res/test_token.wasm", + REFERENDUM_WASM_BYTES => "../res/referendum_local.wasm", +} + + + +pub fn init_env(register_user: bool) -> (UserAccount, UserAccount, UserAccount, ContractAccount, ContractAccount) { + let root = init_simulator(None); + + let owner = root.create_user("owner".to_string(), to_yocto("100")); + let user = root.create_user("user".to_string(), to_yocto("100")); + + let xref_contract = deploy!( + contract: TestToken, + contract_id: "xref", + bytes: &TEST_WASM_BYTES, + signer_account: root + ); + call!(root, xref_contract.new("xref".to_string(), "xref".to_string(), 18)).assert_success(); + call!(owner, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(user, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + + call!(root, xref_contract.mint(owner.valid_account_id(), to_yocto("10000").into())).assert_success(); + call!(root, xref_contract.mint(user.valid_account_id(), to_yocto("100").into())).assert_success(); + + let referendum_contract = deploy!( + contract: Referendum, + contract_id: "referendum", + bytes: &REFERENDUM_WASM_BYTES, + signer_account: root + ); + call!(root, referendum_contract.new(owner.valid_account_id(), xref_contract.valid_account_id())).assert_success(); + call!(root, xref_contract.storage_deposit(Some(referendum_contract.valid_account_id()), None), deposit = to_yocto("1")).assert_success(); + if register_user { + let current_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + owner, + referendum_contract.modify_genesis_timestamp(nano_to_sec(current_timestamp) + 10) + ) + .assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + } + (root, owner, user, xref_contract, referendum_contract) +} + +pub fn init_proposal_users(root: &UserAccount, xref_contract: &ContractAccount, referendum_contract: &ContractAccount) -> (UserAccount, UserAccount, UserAccount){ + let proposal_user = root.create_user("proposal_user".to_string(), to_yocto("100")); + let vote_user1 = root.create_user("vote_user1".to_string(), to_yocto("100")); + let vote_user2 = root.create_user("vote_user2".to_string(), to_yocto("100")); + + call!(proposal_user, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(vote_user1, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(vote_user2, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + + call!(proposal_user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(vote_user1, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(vote_user2, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + + call!(root, xref_contract.mint(proposal_user.valid_account_id(), to_yocto("100").into())).assert_success(); + call!(root, xref_contract.mint(vote_user1.valid_account_id(), to_yocto("100").into())).assert_success(); + call!(root, xref_contract.mint(vote_user2.valid_account_id(), to_yocto("100").into())).assert_success(); + + (proposal_user, vote_user1, vote_user2) +} \ No newline at end of file diff --git a/referendum/tests/common/mod.rs b/referendum/tests/common/mod.rs new file mode 100644 index 0000000..d050952 --- /dev/null +++ b/referendum/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod init; +pub mod utils; \ No newline at end of file diff --git a/referendum/tests/common/utils.rs b/referendum/tests/common/utils.rs new file mode 100644 index 0000000..4a3125b --- /dev/null +++ b/referendum/tests/common/utils.rs @@ -0,0 +1,102 @@ +#![allow(unused)] +use near_sdk_sim::{ExecutionResult, view}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::json_types::U128; +use near_sdk::AccountId; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct AccountInfo { + pub locking_amount: U128, + pub ballot_amount: U128, + pub unlocking_session_id: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[serde(crate = "near_sdk::serde")] +pub struct SessionState { + pub session_id: u32, + pub expire_amount: U128, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct ContractMetadata { + pub owner_id: AccountId, + pub locked_token: AccountId, + pub genesis_timestamp_sec: u32, + pub cur_session_id: u32, + pub cur_total_ballot: U128, + pub cur_lock_amount: U128, + pub last_proposal_id: u32, + pub lock_amount_per_proposal: U128, + pub account_number: u64, + pub vote_policy: Vec, + pub nonsense_threshold: Rational, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum ProposalKind { + Vote, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum ProposalStatus { + WarmUp, + InProgress, + Approved, + Rejected, + Nonsense, + Expired, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct ProposalInfo{ + pub id: u32, + pub proposer: AccountId, + pub lock_amount: U128, + pub description: String, + pub vote_policy: VotePolicy, + pub kind: ProposalKind, + pub status: ProposalStatus, + pub vote_counts: [U128; 4], + pub session_id: u32, + pub start_offset_sec: u32, + pub lasts_sec: u32, +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +#[serde(crate = "near_sdk::serde")] +pub struct Rational { + pub numerator: u32, + pub denominator: u32, +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] +#[serde(crate = "near_sdk::serde")] +pub enum VotePolicy { + Relative(Rational, Rational), + Absolute(Rational, Rational), +} + +pub fn get_error_count(r: &ExecutionResult) -> u32 { + r.promise_errors().len() as u32 +} + +pub fn get_error_status(r: &ExecutionResult) -> String { + format!("{:?}", r.promise_errors()[0].as_ref().unwrap().status()) +} + +pub fn nano_to_sec(nano: u64) -> u32 { + (nano / 1_000_000_000) as u32 +} + +pub fn sec_to_nano(sec: u32) -> u64 { + sec as u64 * 1_000_000_000 +} \ No newline at end of file diff --git a/referendum/tests/owner.rs b/referendum/tests/owner.rs new file mode 100644 index 0000000..180d2e0 --- /dev/null +++ b/referendum/tests/owner.rs @@ -0,0 +1,44 @@ +use near_sdk_sim::{call, view, to_yocto}; +use near_sdk::json_types::U128; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_owner(){ + let (root, owner, user, _, referendum_contract) = + init_env(false); + + call!( + owner, + referendum_contract.set_owner(user.valid_account_id()) + ).assert_success(); + + let out_come = call!( + owner, + referendum_contract.set_owner(user.valid_account_id()) + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOT_ALLOWED")); + + let current_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + user, + referendum_contract.modify_genesis_timestamp(nano_to_sec(current_timestamp) + 10) + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.genesis_timestamp_sec, nano_to_sec(current_timestamp) + 10); + + assert_eq!(contract_metadata.lock_amount_per_proposal.0, to_yocto("10")); + call!( + user, + referendum_contract.modify_endorsement_amount(U128(to_yocto("20"))) + ).assert_success(); + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.lock_amount_per_proposal.0, to_yocto("20")); +} \ No newline at end of file diff --git a/referendum/tests/test_lock.rs b/referendum/tests/test_lock.rs new file mode 100644 index 0000000..f6ea127 --- /dev/null +++ b/referendum/tests/test_lock.rs @@ -0,0 +1,285 @@ +use near_sdk_sim::{call, view, to_yocto}; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_lock_user_not_register(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let user = root.create_user("user_not_register".to_string(), to_yocto("100")); + call!(user, xref_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(root, xref_contract.mint(user.valid_account_id(), to_yocto("100").into())).assert_success(); + + let out_come = call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "10".to_string()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_USER_NOT_REGISTER")); +} + +#[test] +fn test_lock_new_lasts1(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10")); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("10")); +} + +#[test] +fn test_lock_new_lasts24(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "24".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10") * 24); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10") * 24); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10") * 24); + assert_eq!(account_info.unlocking_session_id, 24); + + let session_state = view!(referendum_contract.get_session_state(23)).unwrap_json::(); + assert_eq!(session_state.session_id, 23); + assert_eq!(session_state.expire_amount.0, to_yocto("10") * 24); +} + +#[test] +fn test_lock_new_when_session_last_day(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 29 * 3600 * 24 * 1_000_000_000; + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("1")); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("30")); + assert_eq!(account_info.ballot_amount.0, to_yocto("1")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("1")); +} + +#[test] +fn test_lock_new_expire(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("10")); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 30 * 3600 * 24 * 1_000_000_000; + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("20")); + assert_eq!(account_info.ballot_amount.0, to_yocto("20")); + assert_eq!(account_info.unlocking_session_id, 2); +} + +#[test] +fn test_lock_append_expire(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("10")); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + let out_come = call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_RUNNING_LOCKING")); +} + +#[test] +fn test_lock_append(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "10".to_string()), + deposit = 1 + ).assert_success(); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("20") * 10); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("20")); + assert_eq!(account_info.ballot_amount.0, to_yocto("20") * 10); + assert_eq!(account_info.unlocking_session_id, 10); + + let session_state = view!(referendum_contract.get_session_state(9)).unwrap_json::(); + assert_eq!(session_state.session_id, 9); + assert_eq!(session_state.expire_amount.0, to_yocto("20") * 10); +} + +#[test] +fn test_lock_middle_append(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "10".to_string()), + deposit = 1 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 5 * 30 * 3600 * 24 * 1_000_000_000; + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 5); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10") * 10 + to_yocto("10") * 5); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("20")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10") * 10 + to_yocto("10") * 5); + assert_eq!(account_info.unlocking_session_id, 10); + + let session_state = view!(referendum_contract.get_session_state(9)).unwrap_json::(); + assert_eq!(session_state.session_id, 9); + assert_eq!(session_state.expire_amount.0, to_yocto("10") * 10 + to_yocto("10") * 5); +} + +#[test] +fn test_lock_append_add_msg(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "10".to_string()), + deposit = 1 + ).assert_success(); + + let out_come = call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "10".to_string()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_EXIST_RUNNING_LOCKING")); +} + +#[test] +fn test_lock_append_when_no_lock(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + let out_come = call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_RUNNING_LOCKING")); +} + +#[test] +fn test_lock_append_illegal_msg(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + let out_come = call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "0".to_string()), + deposit = 1 + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_ILLEGAL_MSG")); +} diff --git a/referendum/tests/test_proposal.rs b/referendum/tests/test_proposal.rs new file mode 100644 index 0000000..e99ba48 --- /dev/null +++ b/referendum/tests/test_proposal.rs @@ -0,0 +1,361 @@ +use near_sdk_sim::{call, view, to_yocto}; +use near_sdk::json_types::U128; +use near_sdk::borsh::BorshSerialize; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_add_proposal(){ + let (root, owner, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let vote_policy = VotePolicy::Relative(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy.try_to_vec().unwrap().into()) + ).assert_success(); + + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ); + + assert_eq!(out_come.unwrap_json::(), 0); + assert!(orig_user_balance - proposal_user.account().unwrap().amount > to_yocto("10")); + assert!(orig_user_balance - proposal_user.account().unwrap().amount < to_yocto("10.11")); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + + assert_eq!(proposal_info.proposer, proposal_user.account_id); + assert_eq!(proposal_info.lock_amount.0, 10_000_000_000_000_000_000_000_000); + assert_eq!(proposal_info.description, "test proposal".to_string()); + assert_eq!(proposal_info.vote_policy, vote_policy); + assert_eq!(proposal_info.kind, ProposalKind::Vote); + assert_eq!(proposal_info.status, ProposalStatus::WarmUp); + assert_eq!(proposal_info.vote_counts, [U128(0); 4]); + assert_eq!(proposal_info.session_id, 1); + assert_eq!(proposal_info.start_offset_sec, 1000); + assert_eq!(proposal_info.lasts_sec, 100000); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.last_proposal_id, 1); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(1)).unwrap_json::>(), [0]); +} + +#[test] +fn test_add_proposal_not_enough_lock_near(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOT_ENOUGH_LOCK_NEAR")); +} + +#[test] +fn test_add_proposal_refund(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let orig_user_balance = proposal_user.account().unwrap().amount; + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 20_000_000_000_000_000_000_000_000 + ).assert_success(); + assert!(orig_user_balance - proposal_user.account().unwrap().amount > to_yocto("10")); + assert!(orig_user_balance - proposal_user.account().unwrap().amount < to_yocto("10.11")); +} + +#[test] +fn test_add_proposal_start_time_lt_current_time(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 1, 7 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_PROPOSAL_START_TIME_NEED_GE_CURRENT_TIME")); +} + +#[test] +fn test_add_proposal_end_time_gt_next_session_begin_time(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 60 * 60, 30 * 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(get_error_count(&out_come), 1); + println!("{}", get_error_status(&out_come)); + assert!(get_error_status(&out_come).contains("ERR_PROPOSAL_END_TIME_NEED_LE_NEXT_SESSION_BEGIN_TIME")); +} + +#[test] +fn test_add_proposal_session_id_before_current_session(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_SESSION_ID_NEED_GE_CURRENT_SESSION_ID")); +} + +#[test] +fn test_remove_proposal_during_warm_up(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + let orig_user_balance = proposal_user.account().unwrap().amount; + assert!(call!( + proposal_user, + referendum_contract.remove_proposal(0) + ).unwrap_json::()); + assert!(proposal_user.account().unwrap().amount - orig_user_balance > to_yocto("9.99")); + assert!(proposal_user.account().unwrap().amount - orig_user_balance < to_yocto("10")); + + assert_eq!(None, view!(referendum_contract.get_proposal_info(0)).unwrap_json::>()); +} + +#[test] +fn test_remove_proposal_during_in_progress(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 60 * 60, 7 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp += 3600 * 24 * 1_000_000_000; + + let out_come = call!( + proposal_user, + referendum_contract.remove_proposal(0) + ); + assert!(!out_come.unwrap_json::()); +} + +#[test] +fn test_remove_proposal_no_proposal(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 60 * 60, 7 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + let out_come = call!( + proposal_user, + referendum_contract.remove_proposal(1) + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_PROPOSAL")); +} + +#[test] +fn test_remove_proposal_not_allow(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 60 * 60, 7 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + let out_come = call!( + user, + referendum_contract.remove_proposal(0) + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOT_ALLOW")); +} + +#[test] +fn test_redeem(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + let out_come = call!( + proposal_user, + referendum_contract.redeem_near_in_expired_proposal(0) + ); + + assert!(!out_come.unwrap_json::()); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + proposal_user, + referendum_contract.redeem_near_in_expired_proposal(0) + ); + + assert!(out_come.unwrap_json::()); + assert!(proposal_user.account().unwrap().amount - orig_user_balance > to_yocto("9.99")); + assert!(proposal_user.account().unwrap().amount - orig_user_balance < to_yocto("10")); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + assert_eq!(proposal_info.status, ProposalStatus::Expired); + assert_eq!(proposal_info.lock_amount.0, 0); +} + +#[test] +fn test_redeem_no_proposal(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + let out_come = call!( + proposal_user, + referendum_contract.redeem_near_in_expired_proposal(1) + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_PROPOSAL")); +} + +#[test] +fn test_redeem_not_allow(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + let out_come = call!( + user, + referendum_contract.redeem_near_in_expired_proposal(0) + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOT_ALLOW")); +} + +#[test] +fn test_proposal_ids_in_session(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, _, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 0); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(1)).unwrap_json::>(), [0]); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 1); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(0)).unwrap_json::>(), [1]); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 1, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 2); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(1)).unwrap_json::>(), [0,2]); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 10, 1000, 100000), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 3); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(10)).unwrap_json::>(), [3]); + + assert!(call!( + proposal_user, + referendum_contract.remove_proposal(2) + ).unwrap_json::()); + + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(1)).unwrap_json::>(), [0]); + +} \ No newline at end of file diff --git a/referendum/tests/test_storage.rs b/referendum/tests/test_storage.rs new file mode 100644 index 0000000..a8a62a9 --- /dev/null +++ b/referendum/tests/test_storage.rs @@ -0,0 +1,150 @@ +use near_sdk_sim::{call, view, to_yocto}; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_storage_deposit_not_launched(){ + let (_, _, user, _, referendum_contract) = + init_env(false); + + let out_come = call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOT_LAUNCHED")); +} + +#[test] +fn test_storage_deposit_normal(){ + let (root, owner, user, _, referendum_contract) = + init_env(false); + + let current_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + owner, + referendum_contract.modify_genesis_timestamp(nano_to_sec(current_timestamp) + 10) + ) + .assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("0.01")).assert_success(); + assert!(orig_user_balance - user.account().unwrap().amount > to_yocto("0.01")); + assert!(orig_user_balance - user.account().unwrap().amount < to_yocto("0.011")); +} + +#[test] +fn test_storage_deposit_repeat(){ + let (root, owner, user, _, referendum_contract) = + init_env(false); + + let current_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + owner, + referendum_contract.modify_genesis_timestamp(nano_to_sec(current_timestamp) + 10) + ) + .assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("0.01")).assert_success(); + assert!(orig_user_balance - user.account().unwrap().amount > to_yocto("0.01")); + assert!(orig_user_balance - user.account().unwrap().amount < to_yocto("0.011")); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("0.01")).assert_success(); + assert!(orig_user_balance - user.account().unwrap().amount < to_yocto("0.001")); +} + +#[test] +fn test_storage_deposit_refund(){ + let (root, owner, user, _, referendum_contract) = + init_env(false); + + let current_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + owner, + referendum_contract.modify_genesis_timestamp(nano_to_sec(current_timestamp) + 10) + ) + .assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + assert!(orig_user_balance - user.account().unwrap().amount > to_yocto("0.01")); + assert!(orig_user_balance - user.account().unwrap().amount < to_yocto("0.011")); +} + +#[test] +fn test_storage_unregister_normal(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_unregister(None), deposit = 1).assert_success(); + assert!(user.account().unwrap().amount - orig_user_balance > to_yocto("0.009")); + assert!(user.account().unwrap().amount - orig_user_balance < to_yocto("0.01")); + + call!(user, referendum_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + call!( + user, + referendum_contract.withdraw(), + deposit = 1 + ).assert_success(); + + call!(user, referendum_contract.storage_unregister(None), deposit = 1).assert_success(); +} + +#[test] +fn test_storage_unregister_repeat(){ + let (_, _, user, _, referendum_contract) = + init_env(true); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_unregister(None), deposit = 1).assert_success(); + assert!(user.account().unwrap().amount - orig_user_balance > to_yocto("0.009")); + assert!(user.account().unwrap().amount - orig_user_balance < to_yocto("0.01")); + + let orig_user_balance = user.account().unwrap().amount; + call!(user, referendum_contract.storage_unregister(None), deposit = 1).assert_success(); + assert!(orig_user_balance - user.account().unwrap().amount < to_yocto("0.001")); +} + +#[test] +fn test_storage_unregister_before_unlock(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let out_come = call!(user, referendum_contract.storage_unregister(None), deposit = 1); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_ACCOUNT_NOT_UNLOCK")); +} + +#[test] +fn storage_withdraw(){ + let (_, _, user, _, referendum_contract) = + init_env(true); + + let out_come = call!(user, referendum_contract.storage_withdraw(None), deposit = 1); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_STORAGE_CAN_WITHDRAW")); +} \ No newline at end of file diff --git a/referendum/tests/test_unlock.rs b/referendum/tests/test_unlock.rs new file mode 100644 index 0000000..4f1bced --- /dev/null +++ b/referendum/tests/test_unlock.rs @@ -0,0 +1,114 @@ +use near_sdk_sim::{call, view, to_yocto}; +use near_sdk::json_types::U128; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_unlock(){ + let (root, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10")); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("10")); + + assert_eq!(view!(xref_contract.ft_balance_of(user.valid_account_id())).unwrap_json::().0, to_yocto("90")); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 31 * 3600 * 24 * 1_000_000_000; + + call!( + user, + referendum_contract.withdraw(), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 1); + assert_eq!(contract_metadata.cur_total_ballot.0, 0); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, 0); + assert_eq!(account_info.ballot_amount.0, 0); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 24); + assert_eq!(session_state.expire_amount.0, 0); + + assert_eq!(view!(xref_contract.ft_balance_of(user.valid_account_id())).unwrap_json::().0, to_yocto("100")); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 1); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("29")); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("30")); + assert_eq!(account_info.ballot_amount.0, to_yocto("29")); + assert_eq!(account_info.unlocking_session_id, 2); + + let session_state = view!(referendum_contract.get_session_state(1)).unwrap_json::(); + assert_eq!(session_state.session_id, 1); + assert_eq!(session_state.expire_amount.0, to_yocto("29")); + + assert_eq!(view!(xref_contract.ft_balance_of(user.valid_account_id())).unwrap_json::().0, to_yocto("70")); +} + +#[test] +fn test_unlock_ahead(){ + let (_, _, user, xref_contract, referendum_contract) = + init_env(true); + + call!( + user, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_session_id, 0); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("10")); + + let account_info = view!(referendum_contract.get_account_info(user.valid_account_id())).unwrap_json::(); + assert_eq!(account_info.locking_amount.0, to_yocto("10")); + assert_eq!(account_info.ballot_amount.0, to_yocto("10")); + assert_eq!(account_info.unlocking_session_id, 1); + + let session_state = view!(referendum_contract.get_session_state(0)).unwrap_json::(); + assert_eq!(session_state.session_id, 0); + assert_eq!(session_state.expire_amount.0, to_yocto("10")); + + assert_eq!(view!(xref_contract.ft_balance_of(user.valid_account_id())).unwrap_json::().0, to_yocto("90")); + + let out_come = call!( + user, + referendum_contract.withdraw(), + deposit = 1 + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NOTHING_CAN_BE_WITHDRAW")); +} \ No newline at end of file diff --git a/referendum/tests/test_vote.rs b/referendum/tests/test_vote.rs new file mode 100644 index 0000000..62b08f0 --- /dev/null +++ b/referendum/tests/test_vote.rs @@ -0,0 +1,476 @@ +use near_sdk_sim::{call, view, to_yocto}; +use near_sdk::borsh::BorshSerialize; +use near_sdk::json_types::U128; +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_vote(){ + let (root, owner, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, vote_user2) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let vote_policy1 = VotePolicy::Relative(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + let vote_policy2 = VotePolicy::Absolute(Rational{numerator:1, denominator:2}, Rational{numerator:2, denominator:2}); + + let remove_rational = Rational{numerator:2, denominator:2}; + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy1.try_to_vec().unwrap().into()) + ).assert_success(); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy2.try_to_vec().unwrap().into()) + ).assert_success(); + + call!( + owner, + referendum_contract.modify_nonsense_threshold(remove_rational.try_to_vec().unwrap().into()) + ).assert_success(); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal1".to_string(), "vote".into(), 0.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 0); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal2".to_string(), "vote".into(), 1.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 1); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal3".to_string(), "vote".into(), 0.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 2); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("60")); + + //proposal begin + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 1 * 3600 * 24 * 1_000_000_000; + + //vote_user1 vote approve to proposal 0 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, to_yocto("10")); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + + assert_eq!(proposal_info.status, ProposalStatus::Approved); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(to_yocto("30")), U128(0), U128(0), U128(to_yocto("60"))]); + + //vote_user1 vote reject to proposal 1 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(1, "reject".into(), Some("reject".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + + assert_eq!(proposal_info.status, ProposalStatus::InProgress); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("30")), U128(0), U128(to_yocto("60"))]); + + //vote_user2 vote reject to proposal 1 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user2, + referendum_contract.act_proposal(1, "reject".into(), Some("reject".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, to_yocto("10")); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + + assert_eq!(proposal_info.status, ProposalStatus::Rejected); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("60")), U128(0), U128(to_yocto("60"))]); + + //vote_user1 vote remove to proposal 2 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(2, "nonsense".into(), Some("nonsense".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + + let out_come = call!( + vote_user2, + referendum_contract.act_proposal(2, "nonsense".into(), Some("nonsense".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(2)).unwrap_json::(); + println!("{:?}", proposal_info); + assert_eq!(proposal_info.status, ProposalStatus::Nonsense); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(0), U128(to_yocto("60")), U128(to_yocto("60"))]); +} + +#[test] +fn test_vote_append(){ + let (root, owner, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, vote_user2) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let vote_policy1 = VotePolicy::Relative(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + let vote_policy2 = VotePolicy::Absolute(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy1.try_to_vec().unwrap().into()) + ).assert_success(); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy2.try_to_vec().unwrap().into()) + ).assert_success(); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal1".to_string(), "vote".into(), 0.into(), 0, 20 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 0); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal2".to_string(), "vote".into(), 1.into(), 0, 20 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 1); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "2".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("90")); + + //proposal begin + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 3600 * 21 * 1_000_000_000; + + //vote_user1 vote approve to proposal 0 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + + assert_eq!(proposal_info.status, ProposalStatus::InProgress); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(to_yocto("30")), U128(0), U128(0), U128(to_yocto("90"))]); + + //vote_user1 append lock + let orig_user_balance = proposal_user.account().unwrap().amount; + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, to_yocto("10")); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + + assert_eq!(proposal_info.status, ProposalStatus::Approved); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(to_yocto("60")), U128(0), U128(0), U128(to_yocto("120"))]); + + //vote_user1 vote reject to proposal 1 + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(1, "reject".into(), Some("reject".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("60")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + + assert_eq!(proposal_info.status, ProposalStatus::InProgress); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("60")), U128(0), U128(to_yocto("140"))]); + + //vote_user1 vote_user1 append lock + let orig_user_balance = proposal_user.account().unwrap().amount; + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, to_yocto("10")); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + + assert_eq!(proposal_info.status, ProposalStatus::Rejected); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("90")), U128(0), U128(to_yocto("170"))]); +} + +#[test] +fn test_vote_append_mutli(){ + let (root, owner, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, vote_user2) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + let vote_policy1 = VotePolicy::Relative(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + let vote_policy2 = VotePolicy::Absolute(Rational{numerator:1, denominator:2}, Rational{numerator:1, denominator:2}); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy1.try_to_vec().unwrap().into()) + ).assert_success(); + + call!( + owner, + referendum_contract.modify_vote_policy(vote_policy2.try_to_vec().unwrap().into()) + ).assert_success(); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal1".to_string(), "vote".into(), 0.into(), 0, 20 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 0); + + let out_come = call!( + proposal_user, + referendum_contract.add_proposal("test proposal2".to_string(), "vote".into(), 1.into(), 0, 20 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ); + assert_eq!(out_come.unwrap_json::(), 1); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "2".to_string()), + deposit = 1 + ).assert_success(); + + let contract_metadata = view!(referendum_contract.contract_metadata()).unwrap_json::(); + assert_eq!(contract_metadata.cur_total_ballot.0, to_yocto("90")); + + //proposal begin + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 3600 * 21 * 1_000_000_000; + assert_eq!(view!(referendum_contract.get_proposal_ids_in_session(0)).unwrap_json::>(), [0,1]); + + //vote_user1 vote approve to proposal 0 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + + assert_eq!(proposal_info.status, ProposalStatus::InProgress); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(to_yocto("30")), U128(0), U128(0), U128(to_yocto("90"))]); + + //vote_user1 vote reject to proposal 1 + let orig_user_balance = proposal_user.account().unwrap().amount; + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(1, "reject".into(), Some("reject".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, 0); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + + assert_eq!(proposal_info.status, ProposalStatus::InProgress); + assert_eq!(proposal_info.lock_amount.0, to_yocto("10")); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("30")), U128(0), U128(to_yocto("90"))]); + + //vote_user1 append lock + let orig_user_balance = proposal_user.account().unwrap().amount; + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "".to_string()), + deposit = 1 + ).assert_success(); + assert_eq!(proposal_user.account().unwrap().amount - orig_user_balance, to_yocto("20")); + + let proposal_info = view!(referendum_contract.get_proposal_info(0)).unwrap_json::(); + println!("{:?}", proposal_info); + assert_eq!(proposal_info.status, ProposalStatus::Approved); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(to_yocto("60")), U128(0), U128(0), U128(to_yocto("120"))]); + + let proposal_info = view!(referendum_contract.get_proposal_info(1)).unwrap_json::(); + println!("{:?}", proposal_info); + assert_eq!(proposal_info.status, ProposalStatus::Rejected); + assert_eq!(proposal_info.lock_amount.0, 0); + assert_eq!(proposal_info.vote_counts, [U128(0), U128(to_yocto("60")), U128(0), U128(to_yocto("120"))]); +} + +#[test] +fn test_vote_no_proposal(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, _) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(1, "approve".into(), Some("approve".to_string())) + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_NO_PROPOSAL")); +} + +#[test] +fn test_vote_not_votable(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, vote_user2) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "5".to_string()), + deposit = 1 + ).assert_success(); + + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_PROPOSAL_NOT_VOTABLE")); +} + +#[test] +fn test_vote_already_vote(){ + let (root, _, _, xref_contract, referendum_contract) = + init_env(true); + + let (proposal_user, vote_user1, vote_user2) = init_proposal_users(&root, &xref_contract, &referendum_contract); + + + + call!( + proposal_user, + referendum_contract.add_proposal("test proposal".to_string(), "vote".into(), 0.into(), 0, 24 * 60 * 60, 24 * 60 * 60), + deposit = 10_000_000_000_000_000_000_000_000 + ).assert_success(); + + call!( + vote_user1, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "1".to_string()), + deposit = 1 + ).assert_success(); + + call!( + vote_user2, + xref_contract.ft_transfer_call(referendum_contract.valid_account_id(), to_yocto("30").into(), None, "5".to_string()), + deposit = 1 + ).assert_success(); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(view!(referendum_contract.contract_metadata()).unwrap_json::().genesis_timestamp_sec) + 1 * 3600 * 24 * 1_000_000_000; + + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + assert_eq!(out_come.unwrap_json::().0, to_yocto("30")); + + let out_come = call!( + vote_user1, + referendum_contract.act_proposal(0, "approve".into(), Some("approve".to_string())) + ); + + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("ERR_ALREADY_VOTED")); +} \ No newline at end of file diff --git a/res/referendum.wasm b/res/referendum.wasm new file mode 100755 index 0000000..44584b7 Binary files /dev/null and b/res/referendum.wasm differ diff --git a/res/referendum_release.wasm b/res/referendum_release.wasm new file mode 100755 index 0000000..29c2bf9 Binary files /dev/null and b/res/referendum_release.wasm differ