diff --git a/.gitignore b/.gitignore index 7107c9f..0ac33e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # Generated by Cargo # will have compiled files and executables /target/ - +/xcheddar/res/ /res +/xcheddar/tests +/neardev # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/Cargo.toml b/Cargo.toml index ce61d77..6425f24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ members = [ # "./p1-staking-pool-dyn", "./p2-token-staking-fixed", "./p3-farm", + "./p3-farm-nft", + "./xcheddar", + "./p4-pool" ] diff --git a/README.md b/README.md index 3b0da17..dd7f691 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,7 @@ Cheddar Network is the leading ecosystem for NEAR dapps. Our mission is to be gr ## Cheddar Defi Farm A Defi token and farm on NEAR. Cheddar is a fun way for NEAR users to collect, swap and send Cheddar. To get Cheddar you can swap NEAR and stake it in the farm to stack even more Cheddar. Cheddar will also include a DAO (Phase II) where users can lock Cheddar to receive governance tokens to participate in the development process while earning additional rewards. + +## XCheddar token + +Token which allows users stake their Cheddar tokens and get some rewards for it. Base model is the same as CRV and [veCRV](https://resources.curve.fi/base-features/understanding-crv) locked tokens model. After 30-days period distribution of rewards starts and it counting from ```reward_per_sec``` reward parameter. Locked token have a virtual price depends on amount of locked/minted XCheddar tokens diff --git a/cheddar/Cargo.toml b/cheddar/Cargo.toml index ae28c66..5334401 100644 --- a/cheddar/Cargo.toml +++ b/cheddar/Cargo.toml @@ -11,10 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] serde = { version = "*", features = ["derive"] } serde_json = "*" -near-sdk = { git = "https://github.com/near/near-sdk-rs", tag="3.1.0" } -near-contract-standards = { git = "https://github.com/near/near-sdk-rs", tag="3.1.0" } uint = { version = "0.9.0", default-features = false } - -[dev-dependencies] -# near-primitives = { git = "https://github.com/nearprotocol/nearcore.git" } -# near-sdk-sim = { git = "https://github.com/near/near-sdk-rs.git", version="v3.1.0" } +near-sys = "0.1.0" +near-contract-standards = "4.0.0" +near-sdk = "4.0.0" diff --git a/cheddar/README.md b/cheddar/README.md index b1c75e7..fe9dba9 100644 --- a/cheddar/README.md +++ b/cheddar/README.md @@ -10,4 +10,4 @@ Main features of Cheddar Coin are: ## Technicalities -The Cheddar Coin implements the `NEP-141` standard. It's a fungible token. +The Cheddar Coin implements the `NEP-141` standard. It's a fungible token. \ No newline at end of file diff --git a/cheddar/src/internal.rs b/cheddar/src/internal.rs index 7d000a7..4fd5141 100644 --- a/cheddar/src/internal.rs +++ b/cheddar/src/internal.rs @@ -1,4 +1,4 @@ -use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::json_types::U128; use near_sdk::{AccountId, Balance, PromiseResult}; use crate::storage::AccBalance; @@ -13,7 +13,7 @@ impl Contract { } #[inline] - pub(crate) fn assert_minter(&self, account_id: String) { + pub(crate) fn assert_minter(&self, account_id: AccountId) { assert!(self.minters.contains(&account_id), "not a minter"); } @@ -123,10 +123,10 @@ impl Contract { pub(crate) fn ft_resolve_transfer_adjust( &mut self, sender_id: &AccountId, - receiver_id: ValidAccountId, + receiver_id: AccountId, amount: U128, ) -> (u128, u128) { - let receiver_id: AccountId = receiver_id.into(); + let amount: Balance = amount.into(); // Get the unused amount from the `ft_on_transfer` call result. diff --git a/cheddar/src/lib.rs b/cheddar/src/lib.rs index 9132e30..2b5f351 100644 --- a/cheddar/src/lib.rs +++ b/cheddar/src/lib.rs @@ -1,5 +1,4 @@ /// Cheddar Token -/// /// Functionality: /// - No account storage complexity - Since NEAR slashed storage price by 10x /// it does not make sense to add that friction (storage backup per user). @@ -11,24 +10,15 @@ /// use near_contract_standards::fungible_token::{ core::FungibleTokenCore, - metadata::{FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC}, - resolver::FungibleTokenResolver, + metadata::{FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC}, }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; -use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::json_types::U128; use near_sdk::{ - assert_one_yocto, env, ext_contract, log, near_bindgen, AccountId, Balance, Gas, + assert_one_yocto, env, log, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue, }; - -const TGAS: Gas = 1_000_000_000_000; -const GAS_FOR_RESOLVE_TRANSFER: Gas = 5 * TGAS; -const GAS_FOR_FT_TRANSFER_CALL: Gas = 25 * TGAS + GAS_FOR_RESOLVE_TRANSFER; -const NO_DEPOSIT: Balance = 0; - -near_sdk::setup_alloc!(); - mod internal; mod migrations; mod storage; @@ -165,9 +155,14 @@ impl Contract { self.metadata.set(&m); } - pub fn set_owner(&mut self, owner_id: ValidAccountId) { + pub fn set_owner(&mut self, owner_id: AccountId) { self.assert_owner(); - self.owner_id = owner_id.as_ref().clone(); + assert!( + env::is_valid_account_id(owner_id.as_bytes()), + "Account @{} is invalid!", + owner_id.clone() + ); + self.owner_id = owner_id.clone(); } /// Get the owner of this account. @@ -240,17 +235,17 @@ impl Contract { #[near_bindgen] impl FungibleTokenCore for Contract { #[payable] - fn ft_transfer(&mut self, receiver_id: ValidAccountId, amount: U128, memo: Option) { + fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { assert_one_yocto(); let sender_id = env::predecessor_account_id(); let amount: Balance = amount.into(); - self.internal_transfer(&sender_id, receiver_id.as_ref(), amount, memo); + self.internal_transfer(&sender_id, &receiver_id, amount, memo); } #[payable] fn ft_transfer_call( &mut self, - receiver_id: ValidAccountId, + receiver_id: AccountId, amount: U128, memo: Option, msg: String, @@ -258,25 +253,35 @@ impl FungibleTokenCore for Contract { assert_one_yocto(); let sender_id = env::predecessor_account_id(); let amount: Balance = amount.into(); - self.internal_transfer(&sender_id, receiver_id.as_ref(), amount, memo); + self.internal_transfer(&sender_id, &receiver_id, amount, memo); // Initiating receiver's call and the callback - // ext_fungible_token_receiver::ft_on_transfer( + // ext_ft calls like this was deprecated in v4.0.0 near-sdk-rs + /* ext_ft_receiver::ft_on_transfer( sender_id.clone(), amount.into(), msg, - receiver_id.as_ref(), + receiver_id.clone(), NO_DEPOSIT, env::prepaid_gas() - GAS_FOR_FT_TRANSFER_CALL, ) .then(ext_self::ft_resolve_transfer( sender_id, - receiver_id.into(), + receiver_id, amount.into(), - &env::current_account_id(), + env::current_account_id(), NO_DEPOSIT, GAS_FOR_RESOLVE_TRANSFER, )) + */ + ext_ft_receiver::ext(receiver_id.clone()) + .with_static_gas(env::prepaid_gas() - GAS_FOR_FT_TRANSFER_CALL) + .ft_on_transfer(sender_id.clone(), amount.into(), msg) + .then( + ext_ft_resolver::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) + .ft_resolve_transfer(sender_id, receiver_id, amount.into()), + ) .into() } @@ -284,42 +289,30 @@ impl FungibleTokenCore for Contract { self.total_supply.into() } - fn ft_balance_of(&self, account_id: ValidAccountId) -> U128 { - self._balance_of(account_id.as_ref()).into() - } -} - -#[near_bindgen] -impl FungibleTokenResolver for Contract { - /// Returns the amount of burned tokens in a corner case when the sender - /// has deleted (unregistered) their account while the `ft_transfer_call` was still in flight. - /// Returns (Used token amount, Burned token amount) - #[private] - fn ft_resolve_transfer( - &mut self, - sender_id: ValidAccountId, - receiver_id: ValidAccountId, - amount: U128, - ) -> U128 { - let sender_id: AccountId = sender_id.into(); - let (used_amount, burned_amount) = - self.ft_resolve_transfer_adjust(&sender_id, receiver_id, amount); - if burned_amount > 0 { - log!("{} tokens burned", burned_amount); - } - return used_amount.into(); - } -} - -#[near_bindgen] -impl FungibleTokenMetadataProvider for Contract { - fn ft_metadata(&self) -> FungibleTokenMetadata { - self.internal_get_ft_metadata() + fn ft_balance_of(&self, account_id: AccountId) -> U128 { + self._balance_of(&account_id).into() } } - #[ext_contract(ext_ft_receiver)] pub trait FungibleTokenReceiver { + /// Called by fungible token contract after `ft_transfer_call` was initiated by + /// `sender_id` of the given `amount` with the transfer message given in `msg` field. + /// The `amount` of tokens were already transferred to this contract account and ready to be used. + /// + /// The method must return the amount of tokens that are *not* used/accepted by this contract from the transferred + /// amount. Examples: + /// - The transferred amount was `500`, the contract completely takes it and must return `0`. + /// - The transferred amount was `500`, but this transfer call only needs `450` for the action passed in the `msg` + /// field, then the method must return `50`. + /// - The transferred amount was `500`, but the action in `msg` field has expired and the transfer must be + /// cancelled. The method must return `500` or panic. + /// + /// Arguments: + /// - `sender_id` - the account ID that initiated the transfer. + /// - `amount` - the amount of tokens that were transferred to this account in a decimal string representation. + /// - `msg` - a string message that was passed with this transfer call. + /// + /// Returns the amount of unused tokens that should be returned to sender, in a decimal string representation. fn ft_on_transfer( &mut self, sender_id: AccountId, @@ -328,8 +321,11 @@ pub trait FungibleTokenReceiver { ) -> PromiseOrValue; } -#[ext_contract(ext_self)] -trait FungibleTokenResolver { +#[ext_contract(ext_ft_resolver)] +pub trait FungibleTokenResolver { + /// Returns the amount of burned tokens in a corner case when the sender + /// has deleted (unregistered) their account while the `ft_transfer_call` was still in flight. + /// Returns (Used token amount, Burned token amount) fn ft_resolve_transfer( &mut self, sender_id: AccountId, @@ -338,6 +334,13 @@ trait FungibleTokenResolver { ) -> U128; } +#[near_bindgen] +impl FungibleTokenMetadataProvider for Contract { + fn ft_metadata(&self) -> FungibleTokenMetadata { + self.internal_get_ft_metadata() + } +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use near_sdk::test_utils::{accounts, VMContextBuilder}; @@ -347,7 +350,7 @@ mod tests { const OWNER_SUPPLY: Balance = 1_000_000_000_000_000_000_000_000_000_000; - fn get_context(predecessor_account_id: ValidAccountId) -> VMContextBuilder { + fn get_context(predecessor_account_id: AccountId) -> VMContextBuilder { let mut builder = VMContextBuilder::new(); builder .current_account_id(accounts(0)) @@ -366,7 +369,7 @@ mod tests { .attached_deposit(1) .predecessor_account_id(accounts(1)) .build()); - contract.mint(&accounts(1).to_string(), OWNER_SUPPLY.into()); + contract.mint(&accounts(1), OWNER_SUPPLY.into()); testing_env!(context.is_view(true).build()); assert_eq!(contract.ft_total_supply().0, OWNER_SUPPLY); @@ -391,7 +394,7 @@ mod tests { .attached_deposit(1) .predecessor_account_id(accounts(2)) .build()); - contract.mint(&accounts(2).to_string(), OWNER_SUPPLY.into()); + contract.mint(&accounts(2), OWNER_SUPPLY.into()); testing_env!(context .storage_usage(env::storage_usage()) diff --git a/cheddar/src/storage.rs b/cheddar/src/storage.rs index 5c5f0a0..33a2a35 100644 --- a/cheddar/src/storage.rs +++ b/cheddar/src/storage.rs @@ -3,7 +3,7 @@ use near_contract_standards::storage_management::{ StorageBalance, StorageBalanceBounds, StorageManagement, }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::json_types::U128; use near_sdk::{assert_one_yocto, env, log, near_bindgen, AccountId, Balance, Promise}; // The storage size in bytes for one account. @@ -32,7 +32,7 @@ impl Contract { ) .is_some() { - env::panic("The account is already registered".as_bytes()); + panic!("The account is already registered"); } } @@ -70,10 +70,7 @@ impl Contract { } Some((account_id, balance.near)) } else { - env::panic( - "Can't unregister the account with the positive balance without force" - .as_bytes(), - ) + panic!("Can't unregister the account with the positive balance without force") } } else { log!("The account {} is not registered", &account_id); @@ -94,7 +91,7 @@ impl StorageManagement for Contract { #[payable] fn storage_deposit( &mut self, - account_id: Option, + account_id: Option, registration_only: Option, ) -> StorageBalance { let amount: Balance = env::attached_deposit(); @@ -137,15 +134,16 @@ impl StorageManagement for Contract { if self.accounts.contains_key(&predecessor_account_id) { match amount { Some(amount) if amount.0 > 0 => { - env::panic( - "The amount is greater than the available storage balance".as_bytes(), + panic!( + "The amount is greater than the available storage balance", ); } _ => storage_balance(), } } else { - env::panic( - format!("The account {} is not registered", &predecessor_account_id).as_bytes(), + panic!( + "The account {} is not registered", + predecessor_account_id ); } } @@ -162,8 +160,8 @@ impl StorageManagement for Contract { } } - fn storage_balance_of(&self, account_id: ValidAccountId) -> Option { - if self.accounts.contains_key(account_id.as_ref()) { + fn storage_balance_of(&self, account_id: AccountId) -> Option { + if self.accounts.contains_key(&account_id) { Some(storage_balance()) } else { None diff --git a/cheddar/src/upgrade.rs b/cheddar/src/upgrade.rs index a9f4f75..3e0de7c 100644 --- a/cheddar/src/upgrade.rs +++ b/cheddar/src/upgrade.rs @@ -1,59 +1,52 @@ -//! Implement all the relevant logic for smart contract upgrade. - -use crate::*; - #[cfg(target_arch = "wasm32")] mod upgrade { - use near_sdk::env::BLOCKCHAIN_INTERFACE; + use near_sdk::env; use near_sdk::Gas; + use crate::Contract; + use near_sys as sys; 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; + use crate::util::*; /// 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. + /// After upgrade we call *pub fn migrate()* on the NEW CONTRACT CODE #[no_mangle] - pub extern "C" fn upgrade() { + pub fn upgrade() { + /// Gas for calling migration call. One Tera - 1 TGas + /// 20 Tgas + pub const GAS_FOR_UPGRADE: Gas = Gas(20_000_000_000_000); + const BLOCKCHAIN_INTERFACE_NOT_SET_ERR: &str = "Blockchain interface not set."; + env::setup_panic_hook(); - env::set_blockchain_interface(Box::new(near_blockchain::NearBlockchain {})); + + ///assert ownership + #[allow(unused_doc_comments)] 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(); + + let current_id = env::current_account_id(); + let migrate_method_name = "migrate".as_bytes().to_vec(); + let attached_gas = env::prepaid_gas() - env::used_gas() - GAS_FOR_UPGRADE; 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, - ); - }); + // Load input (NEW CONTRACT CODE) into register 0. + sys::input(0); + // prepare self-call promise + let promise_id = sys::promise_batch_create(current_id.as_bytes().len() as _, current_id.as_bytes().as_ptr() as _); + + // #Action_1 - deploy/upgrade code from register 0 + sys::promise_batch_action_deploy_contract(promise_id, u64::MAX as _, 0); + // #Action_2 - schedule a call for migrate + // Execute on NEW CONTRACT CODE + sys::promise_batch_action_function_call( + promise_id, + migrate_method_name.len() as _, + migrate_method_name.as_ptr() as _, + 0 as _, + 0 as _, + 0 as _, + u64::from(attached_gas), + ); } } -} +} \ No newline at end of file diff --git a/cheddar/src/util.rs b/cheddar/src/util.rs index 3d8e528..773af06 100644 --- a/cheddar/src/util.rs +++ b/cheddar/src/util.rs @@ -1,9 +1,18 @@ use near_sdk::json_types::{U128, U64}; +use near_sdk::Gas; use uint::construct_uint; pub type U128String = U128; pub type U64String = U64; +/// One Tera gas (Tgas), which is 10^12 gas units. +#[allow(dead_code)] +pub const ONE_TERA: Gas = Gas(1_000_000_000_000); +/// 5 Tgas +pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); +/// 30 Tgas (25 Tgas + GAS_FOR_RESOLVE_TRANSFER) +pub const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas(30_000_000_000_000); + construct_uint! { /// 256-bit unsigned integer. pub struct U256(4); diff --git a/p3-farm-nft/Cargo.toml b/p3-farm-nft/Cargo.toml new file mode 100644 index 0000000..6818a89 --- /dev/null +++ b/p3-farm-nft/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "p3-farm-nft" +version = "0.1.0" +authors = ["Guacharo"] +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +serde = { version = "*", features = ["derive"] } +serde_json = { version = "1.0" } +uint = { version = "0.9.0", default-features = false } +near-sdk = "4.0.0" +near-contract-standards = "4.0.0" diff --git a/p3-farm-nft/Makefile b/p3-farm-nft/Makefile new file mode 100644 index 0000000..3f4dba6 --- /dev/null +++ b/p3-farm-nft/Makefile @@ -0,0 +1,15 @@ +################# +# NEARswap # + + +include ../Makefile_common.mk + +export NCLP_ACC=beta-1.nearswap.testnet + +deploy-nearswap: + near deploy --wasmFile target/wasm32-unknown-unknown/release/near_clp.wasm --accountId $(NCLP_ACC) --initFunction "new" --initArgs "{\"owner\": \"$NMASTER_ACC\"}" + +init-nearswap: + @echo near sent ${NMASTER_ACC} ${NCLP_ACC} 200 +# no need to call new because we call it during the deployment +# @echo near call ${NCLP_ACC} new "{\"owner\": \"$NMASTER_ACC\"}" --accountId ${NCLP_ACC} diff --git a/p3-farm-nft/P3_nft_explanation.md b/p3-farm-nft/P3_nft_explanation.md new file mode 100644 index 0000000..4c2658e --- /dev/null +++ b/p3-farm-nft/P3_nft_explanation.md @@ -0,0 +1,74 @@ +# P3 (NFT versioned) explanation +## Function call sequences + +#### fn storage_deposit () => StorageBalance +Registration in P3 contract. +Return StorageBalance. +```rust +StorageBalance { + total: STORAGE_COST.into(), + available: U128::from(0), + } +``` +Reqiire attached deposit which equals to ```STORAGE_COST``` from ```constants.rs``` + +#### CROSS-CALL catching from stakeing token <=> fn nft_transfer_call(args) +##### msg from args is "cheddy" => receive cheddy NFT and insert this one into user Vault +##### msg from args is "to farm" => fn internal_nft_stake () => bool +Staking / Add Cheddy NFT boost depends on ```msg``` from ```nft_transfer_call``` +Panics when: +- In case of ToFarm when contract is paused +- In case of this called not as cross-contract call +- In case of NFT owner not a signer +Refund transfered token: +- Not allowed for stakeing NFT transfered into stake/boost +- Not registered signer (NFT owner) +- In case of Cheddy boost if it already was added to this user Vault before +- Wrong message or no message + +#### [ fn internal_nft_stake () => bool ] (private) +Main logic for stake to farm. +Return ```true``` after vault changing and recomputing stake. +Return ```false``` if NFT not allowed to stake + +Takes ```nft_contract_id``` and ```token_id``` from ```nft_transfer_call(args)``` and insert this to user vault as +stake tokens(```vault.staked```) in format: +```rust + + [ [token_i, token_i, token_i...], [token_i, token_i, token_i...],... [token_i, token_i, token_i...] ] +// ^------nft_contract_i--------^ ^------nft_contract_i--------^ ^------nft_contract_i--------^ +``` +Stake recomputing based on this token ids. Every time we are stake one token and it compute length of current user ```vault.staked[i].len()``` for this nft_contrtact_i. +Farmed tokens counting based on amount of tokens which is actually as like in FT farming, but amount of NFT token units introduced as like ```vault.staked[i].len() * E24```. For example if we stake 1 token with stake RATE, our farmed units or stake will be counted based on 1e24 * RATE / 1e24 = RATE (see ```min_stake``` and ```farmed_tokens``` functions) + +#### fn status (user) => Status +View method for seeing your current stats +```rust +pub struct Status { + /// [ [token_i, token_i,...],... ] where each nft_contract is index + pub stake_tokens: Vec, + /// the min stake based on stake rates and amount of staked tokens + pub stake: U128, + /// Amount of accumulated, not withdrawn farmed units. This is the base farming unit which + /// is translated into `farmed_tokens`. + pub farmed_units: U128, + /// Amount of accumulated, not withdrawn farmed tokens in the same order as + /// contract `farm_tokens`. Computed based on `farmed_units` and the contarct + /// `farmed_token_rates.` + pub farmed_tokens: Vec, + /// token ID of a staked Cheddy. Empty if user doesn't stake any Cheddy. + pub cheddy_nft: String, + /// timestamp (in seconds) of the current round. + pub timestamp: u64, +} +``` +#### fn withdraw_crop () +Withdraw harvested rewards before farm ends/close. Don't closed account +Panics when +- Contract not active +- If predecessor didn't have a staked tokens + +#### fn unstake (args) => Vec +- Unstake given token_id from nt_contract_id +- If token_id not declared - unstakes all tokens from this contract +- If user doesn't have any staked tokens for all contracts closes account diff --git a/p3-farm-nft/README.md b/p3-farm-nft/README.md new file mode 100644 index 0000000..38eb996 --- /dev/null +++ b/p3-farm-nft/README.md @@ -0,0 +1,79 @@ +# P3 NFT Token Farm with Many Staked and Many Farmed token types. + +The P3-fixed farm allows to stake NFT tokens and farm FT. Constraints: + +- The total supply of farmed tokens is fixed = `total_harvested`. This is computed by `reward_rate * number_rounds`. +- Cheddar/FT is farmed per round. During each round we farm `total_ft/number_rounds`. +- Each user, in each round will farm proportionally to the amount of NFT tokens (s)he staked. + +The contract rewards algorithm is based on the ["Scalable Reward Distribution on the Ethereum +Blockchain"](https://uploads-ssl.webflow.com/5ad71ffeb79acc67c8bcdaba/5ad8d1193a40977462982470_scalable-reward-distribution-paper.pdf) algorithm. + +## Parameters + +- Round duration: 1 minute + +## Setup + +1. Deploy contract and init +2. Register farm in token contract before. Then deposit required NEP-141 tokens (`farm_tokens`) +3. Activate by calling `finalize_setup()`. Must be done at least 12h before opening the farm. + +## User Flow + +Let's define a common variables: + +```sh +# address of the farm +FARM= +# reward token address +CHEDDAR=token-v3.cheddar.testnet +REF=ref.fakes.testnet +# the nft contract address(could be more then one) & token_ids we stake +STAKEING_NFT_CONTRACT_ONE= +TOKEN_ID_ONE= +TOKEN_ID_TWO= +# cheddy +CHEDDY_NFT_CONTRACT=cheddy.testnet +# owner +OWNER= +# user +USER=$USER +``` + +1. Register in the farm: + + ```bash + near call $FARM storage_deposit '{}' --accountId $USER --deposit 0.06 + ``` + +2. Stake tokens: + + ```bash + near call $STAKEING_NFT_CONTRACT_ONE nft_transfer_call '{"sender_id": "'$USER'", "previous_owner_id":"'$USER'", "token_id":"'$TOKEN_ID_ONE'", "msg": "to farm"}' --accountId $USER --depositYocto 1 --gas=200000000000000 + ``` + - Add your cheddy boost! + ```bash + near call $CHEDDY_NFT_CONTRACT nft_transfer_call '{"sender_id": "'$USER'", "previous_owner_id":"'$USER'", "token_id":"1", "msg": "cheddy"}' --accountId $USER --depositYocto 1 --gas=200000000000000 + ``` + +3. Enjoy farming, stake more, and observe your status: + + ```bash + near view $FARM status '{"account_id": "'$USER'"}' + ``` + +4. Harvest rewards (if you like to get your CHEDDAR before the farm closes): + + ```bash + near call $FARM withdraw_crop '' --accountId $USER + ``` + +5. Harvest all rewards and close the account (un-register) after the farm will close: + ```bash + near call $FARM close '' --accountId $USER --depositYocto 1 --gas=200000000000000 + ``` + Or u can unstake all (from declared nft contract) - it automatically close account if it was last staked contract + ```bash + near call $FARM unstake '{"nft_contract_id":"'$STAKEING_NFT_CONTRACT_ONE'"}' --accountId $USER --depositYocto 1 --gas=200000000000000 + ``` diff --git a/p3-farm-nft/TODO.org b/p3-farm-nft/TODO.org new file mode 100644 index 0000000..9e4f8d3 --- /dev/null +++ b/p3-farm-nft/TODO.org @@ -0,0 +1,8 @@ +1. Rewards thinking - 1 token staked actually, so we need less RATE then with FT tokens +2. Add functions: + pay-and-stake with cheddar payments for stake or unstake + unstake-all (done, with Option = None) + +Simplification: +* Rewards every near block (approx 1 second), use multiplication + diff --git a/p3-farm-nft/callbacks.md b/p3-farm-nft/callbacks.md new file mode 100644 index 0000000..1ca07c1 --- /dev/null +++ b/p3-farm-nft/callbacks.md @@ -0,0 +1,37 @@ +# Callback sequences / rollbacks check + +#### fn return_tokens => fn return_tokens_callback (OK) +``` + self.return_tokens(a.clone(), amount) + .then(ext_self::return_tokens_callback( + a, + amount, + &env::current_account_id(), + 0, + GAS_FOR_MINT_CALLBACK, + )) + + + // + // schedules an async call to ft_transfer to return staked-tokens to the user + // + fn return_tokens(&self, user: AccountId, amount: U128) -> Promise { + return ext_ft::ft_transfer( + user, + amount, + Some("unstaking".to_string()), + &self.staking_token, + 1, + GAS_FOR_FT_TRANSFER, + ); + } + + // callback for return_tokens + // + pub fn return_tokens_callback(&mut self, user: AccountId, amount: U128) { + + verifies ft_transfer result + in case of failure, restore token amount to user vault +``` + + diff --git a/p3-farm-nft/changes.txt b/p3-farm-nft/changes.txt new file mode 100644 index 0000000..ae91392 --- /dev/null +++ b/p3-farm-nft/changes.txt @@ -0,0 +1,5 @@ ++ using a list for staked and farmed tokens (we can stake multiple token and farm multiple tokens) ++ names have changed -> see the Contract struc ++ status returns a struct -> Status struct + ++ unstake takes a new argument - which token to unstake diff --git a/p3-farm-nft/src/constants.rs b/p3-farm-nft/src/constants.rs new file mode 100644 index 0000000..9f2638d --- /dev/null +++ b/p3-farm-nft/src/constants.rs @@ -0,0 +1,36 @@ +use near_sdk::{Balance, Gas, AccountId}; +/// Gas constants +/// Amount of gas for fungible token transfers. +pub const TGAS: u64 = Gas::ONE_TERA.0; + +pub const GAS_FOR_FT_TRANSFER: Gas = Gas(10 * TGAS); +pub const GAS_FOR_NFT_TRANSFER: Gas = Gas(20 * TGAS); +pub const GAS_FOR_CALLBACK: Gas = Gas(20 * TGAS); + +/// Contract constants ( Stake & Farm ) +/// one second in nanoseconds +pub const SECOND: u64 = 1_000_000_000; +/// round duration in seconds +pub const ROUND: u64 = 60; // 1 minute +pub const ROUND_NS: u64 = 60 * 1_000_000_000; // round duration in nanoseconds +pub const MAX_STAKE: Balance = E24 * 100_000; +/// accumulator overflow, used to correctly update the self.s accumulator. +// TODO: need to addjust it accordingly to the reward rate and the staked token. +// Eg: if +pub const ACC_OVERFLOW: Balance = 10_000_000; // 1e7 + +/// NEAR Constants +pub const NEAR_TOKEN:&str = "near"; +const MILLI_NEAR: Balance = 1000_000000_000000_000000; // 1e21 yoctoNear +pub const STORAGE_COST: Balance = MILLI_NEAR * 60; // 0.06 NEAR +/// E24 is 1 in yocto (1e24 yoctoNear) +pub const E24: Balance = MILLI_NEAR * 1_000; +pub const ONE_YOCTO: Balance = 1; + + +/// NFT constants +pub(crate) type NftContractId = AccountId; +/// NFT Delimeter +// pub const NFT_DELIMETER: &str = "@"; +/// Cheddy boost constant +pub const BASIS_P: Balance = 10_000; \ No newline at end of file diff --git a/p3-farm-nft/src/errors.rs b/p3-farm-nft/src/errors.rs new file mode 100644 index 0000000..7f7a084 --- /dev/null +++ b/p3-farm-nft/src/errors.rs @@ -0,0 +1,8 @@ +// Token registration + +pub const ERR10_NO_ACCOUNT: &str = "E10: account not found. Register the account."; + +// Token Deposit errors // + +// TOKEN STAKED +pub const ERR30_NOT_ENOUGH_STAKE: &str = "E30: not enough staked tokens"; \ No newline at end of file diff --git a/p3-farm-nft/src/helpers.rs b/p3-farm-nft/src/helpers.rs new file mode 100644 index 0000000..e411021 --- /dev/null +++ b/p3-farm-nft/src/helpers.rs @@ -0,0 +1,80 @@ +use std::convert::TryInto; + +use near_contract_standards::non_fungible_token::TokenId; +use near_sdk::json_types::U128; +use near_sdk::{AccountId, Balance}; + +use crate::constants::*; +use crate::vault::TokenIds; + +use uint::construct_uint; +construct_uint! { + /// 256-bit unsigned integer. + pub struct U256(4); +} + +pub fn farmed_tokens(units: u128, rate: Balance) -> Balance { + println!("token_units(staked nft for this contract): {} ", units); + println!("rate for them {} ", rate); + (U256::from(units) * U256::from(rate) / big_e24()).as_u128() +} + +#[allow(non_snake_case)] +pub fn to_U128s(v: &Vec) -> Vec { + v.iter().map(|x| U128::from(*x)).collect() +} + +pub fn find_acc_idx(acc: &AccountId, acc_v: &Vec) -> Option { + Some(acc_v.iter().position(|x| x == acc).expect("invalid nft contract")) +} +pub fn find_token_idx(token: &TokenId, token_v: &Vec) -> Option { + Some(token_v.iter().position(|x| x == token).expect("invalid token")) +} + +pub fn min_stake(staked: &Vec, stake_rates: &Vec) -> Balance { + let mut min = std::u128::MAX; + for (i, rate) in stake_rates.iter().enumerate() { + println!("staked tokens for nft_contract[i]: {:?}", staked[i]); + let staked_tokens:u128 = staked[i].len() as u128 * E24; // Number of NFT tokens for nft_contract[i] as e24 + let s = farmed_tokens(staked_tokens, *rate); + if s < min { + min = s; + } + } + return min; +} + +pub fn all_zeros(v: &Vec) -> bool { + for x in v { + if !x.is_empty() { + return false; + } + } + return true; +} + +/// computes round number based on timestamp in seconds +pub fn round_number(start: u64, end: u64, mut now: u64) -> u64 { + if now < start { + return 0; + } + // we start rounds from 0 + let mut adjust = 0; + if now >= end { + now = end; + // if at the end of farming we don't start a new round then we need to force a new round + if now % ROUND != 0 { + adjust = 1 + }; + } + let r: u64 = ((now - start) / ROUND).try_into().unwrap(); + r + adjust +} + +pub fn near() -> AccountId { + NEAR_TOKEN.parse::().unwrap() +} + +pub fn big_e24() -> U256 { + U256::from(E24) +} diff --git a/p3-farm-nft/src/interfaces.rs b/p3-farm-nft/src/interfaces.rs new file mode 100644 index 0000000..57c6b71 --- /dev/null +++ b/p3-farm-nft/src/interfaces.rs @@ -0,0 +1,106 @@ +use near_sdk::json_types::U128; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{ext_contract, AccountId}; + +use crate::vault::TokenIds; + +// #[ext_contract(ext_staking_pool)] +pub trait StakingPool { + // #[payable] + fn stake(&mut self, amount: U128); + + // #[payable] + fn unstake(&mut self, amount: U128) -> U128; + + fn withdraw_crop(&mut self, amount: U128); + + /****************/ + /* View methods */ + /****************/ + + /// Returns amount of staked NEAR and farmed CHEDDAR of given account & the unix-timestamp for the calculation. + fn status(&self, account_id: AccountId) -> (U128, U128, u64); +} + +#[ext_contract(ext_self)] +pub trait ExtSelf { + fn transfer_staked_callback( + &mut self, + user: AccountId, + token_i: usize, + amount: U128, + fee: U128, + ); + fn transfer_farmed_callback(&mut self, user: AccountId, token_i: usize, amount: U128); + fn withdraw_nft_callback(&mut self, user: AccountId, cheddy: String); + fn withdraw_fees_callback(&mut self, token_i: usize, amount: U128); + fn mint_callback(&mut self, user: AccountId, amount: U128); + fn mint_callback_finally(&mut self); + fn close_account(&mut self, user: AccountId); +} + +#[ext_contract(ext_ft)] +pub trait FungibleToken { + fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option); + fn ft_mint(&mut self, receiver_id: AccountId, amount: U128, memo: Option); +} + +#[ext_contract(ext_nft)] +pub trait NonFungibleToken { + fn nft_transfer_call( + &mut self, + receiver_id: AccountId, + token_id: String, + approval_id: Option, + memo: Option, + msg: String + ); + fn nft_transfer( + &mut self, + receiver_id: AccountId, + token_id: String, + approval_id: Option, + memo: Option, + ); +} +// TODO +#[derive(Debug, Deserialize, Serialize)] +pub struct ContractParams { + pub is_active: bool, + pub owner_id: AccountId, + pub stake_tokens: Vec, + pub stake_rates: Vec, + pub farm_unit_emission: U128, + pub farm_tokens: Vec, + pub farm_token_rates: Vec, + pub farm_deposits: Vec, + pub farming_start: u64, + pub farming_end: u64, + /// NFT token used for boost + pub cheddar_nft: AccountId, + pub total_staked: Vec, + /// total farmed is total amount of tokens farmed (not necessary minted - which would be + /// total_harvested). + pub total_farmed: Vec, + pub fee_rate: U128, + /// Number of accounts currently registered. + pub accounts_registered: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Status { + pub stake_tokens: Vec, + /// the min stake + pub stake: U128, + /// Amount of accumulated, not withdrawn farmed units. This is the base farming unit which + /// is translated into `farmed_tokens`. + pub farmed_units: U128, + /// Amount of accumulated, not withdrawn farmed tokens in the same order as + /// contract `farm_tokens`. Computed based on `farmed_units` and the contarct + /// `farmed_token_rates.` + pub farmed_tokens: Vec, + /// token ID of a staked Cheddy. Empty if user doesn't stake any Cheddy. + pub cheddy_nft: String, + /// timestamp (in seconds) of the current round. + pub timestamp: u64, +} diff --git a/p3-farm-nft/src/lib.rs b/p3-farm-nft/src/lib.rs new file mode 100644 index 0000000..45dade7 --- /dev/null +++ b/p3-farm-nft/src/lib.rs @@ -0,0 +1,1581 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LookupMap; +use near_sdk::json_types::U128; +use near_sdk::{ + assert_one_yocto, env, log, near_bindgen, AccountId, Balance, BorshStorageKey, PanicOnDefault, Promise, + PromiseOrValue, PromiseResult, +}; +use near_contract_standards::non_fungible_token::TokenId; + +pub mod constants; +pub mod errors; +pub mod interfaces; +pub mod helpers; +pub mod vault; +pub mod token_standards; +pub mod storage_management; + +use crate::helpers::*; +use crate::interfaces::*; +use crate::{constants::*, errors::*, vault::*}; + +/// Implementing the "Scalable Reward Distribution on the Ethereum Blockchain" +/// algorithm: +/// https://uploads-ssl.webflow.com/5ad71ffeb79acc67c8bcdaba/5ad8d1193a40977462982470_scalable-reward-distribution-paper.pdf +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct Contract { + /// Farming status + pub is_active: bool, + pub setup_finalized: bool, + pub owner_id: AccountId, + /// Treasury address - a destination for the collected fees. + pub treasury: AccountId, + /// user vaults + pub vaults: LookupMap, + /// Nft contract ids allowed to stake in farm + pub stake_nft_tokens: Vec, + /// total number of units currently staked + pub staked_nft_units: u128, + /// Rate between the staking token and stake units. + /// When farming the `min(staking_token[i]*stake_rate[i]/1e24)` is taken + /// to allocate farm_units. + pub stake_rates: Vec, + pub farm_tokens: Vec, + /// Ratios between the farm unit and all farm tokens when computing reward. + /// When farming, for each token index i in `farm_tokens` we allocate to + /// a user `vault.farmed*farm_token_rates[i]/1e24`. + /// Farmed tokens are distributed to all users proportionally to their stake. + pub farm_token_rates: Vec, + /// amount of $farm_units farmed during each round. Round duration is defined in constants.rs + /// Farmed $farm_units are distributed to all users proportionally to their stake. + pub farm_unit_emission: u128, + /// received deposits for farming reward + pub farm_deposits: Vec, + /// unix timestamp (seconds) when the farming starts. + pub farming_start: u64, + /// unix timestamp (seconds) when the farming ends (first time with no farming). + pub farming_end: u64, + /// NFT token used for boost + pub cheddar_nft: AccountId, + /// boost when staking cheddy in basis points + pub cheddar_nft_boost: u32, + /// total number of harvested farm tokens + pub total_harvested: Vec, + /// rewards accumulator: running sum of farm_units per token (equals to the total + /// number of farmed unit tokens). + reward_acc: u128, + /// round number when the s was previously updated. + reward_acc_round: u64, + /// total amount of currently staked tokens. + total_stake: Vec, + /// total number of accounts currently registered. + pub accounts_registered: u64, + /// Free rate in basis points. The fee is charged from the user staked tokens + /// on withdraw. Example: if fee=2 and user withdraws 10000e24 staking tokens + /// then the protocol will charge 2e24 staking tokens. + pub fee_rate: u128, + /// amount of fee collected (in staking token). + pub fee_collected: Vec, +} + +#[near_bindgen] +impl Contract { + /// Initializes the contract with the account where the NEP-141 token contract resides, start block-timestamp & rewards_per_year. + /// Parameters: + /// * `stake_tokens`: tokens we are staking, cheddar should be one of them. + /// * `farming_start` & `farming_end` are unix timestamps (in seconds). + /// * `fee_rate`: the Contract.fee parameter (in basis points) + /// The farm starts desactivated. To activate, you must send required farming deposits and + /// call `self.finalize_setup()`. + #[init] + pub fn new( + owner_id: AccountId, + stake_nft_tokens: Vec, + stake_rates: Vec, + farm_unit_emission: U128, + farm_tokens: Vec, + farm_token_rates: Vec, + farming_start: u64, + farming_end: u64, + cheddar_nft: AccountId, + cheddar_nft_boost: u32, + fee_rate: u32, + treasury: AccountId, + ) -> Self { + assert!( + farming_start > env::block_timestamp() / SECOND, + "start must be in the future" + ); + assert!(farming_end > farming_start, "End must be after start"); + // TODO: why? + assert!(stake_rates[0].0 == E24, "stake_rate[0] must be 1e24"); + // assert!( + // farm_token_rates[0].0 == E24, + // "farm_token_rates[0] must be 1e24" + // ); + let stake_len = stake_nft_tokens.len(); + let farm_len = farm_tokens.len(); + let c = Self { + is_active: true, + setup_finalized: false, + owner_id, + treasury, + vaults: LookupMap::new(b"v".to_vec()), + stake_nft_tokens, + staked_nft_units: 0, + stake_rates: stake_rates.iter().map(|x| x.0).collect(), + farm_tokens, + farm_token_rates: farm_token_rates.iter().map(|x| x.0).collect(), + farm_unit_emission: farm_unit_emission.0, + farm_deposits: vec![0; farm_len], + farming_start, + farming_end, + cheddar_nft: cheddar_nft.into(), + cheddar_nft_boost, + total_harvested: vec![0; farm_len], + reward_acc: 0, + reward_acc_round: 0, + total_stake: vec![0; stake_len], + accounts_registered: 0, + fee_rate: fee_rate.into(), + fee_collected: vec![0; stake_len], + }; + c.check_vectors(); + c + } + + fn check_vectors(&self) { + let fl = self.farm_tokens.len(); + let sl = self.stake_nft_tokens.len(); + assert!( + fl == self.farm_token_rates.len() + && fl == self.total_harvested.len() + && fl == self.farm_deposits.len(), + "farm token vector length is not correct" + ); + assert!( + sl == self.stake_rates.len() + && sl == self.total_stake.len() + && sl == self.fee_collected.len(), + "stake token vector length is not correct" + ); + } + + // ************ // + // view methods // + // ************ // + + /// Returns amount of staked NEAR and farmed CHEDDAR of given account. + pub fn get_contract_params(&self) -> ContractParams { + ContractParams { + owner_id: self.owner_id.clone(), + stake_tokens: self.stake_nft_tokens.clone(), + stake_rates: to_U128s(&self.stake_rates), + farm_unit_emission: self.farm_unit_emission.into(), + farm_tokens: self.farm_tokens.clone(), + farm_token_rates: to_U128s(&self.farm_token_rates), + farm_deposits: to_U128s(&self.farm_deposits), + is_active: self.is_active, + farming_start: self.farming_start, + farming_end: self.farming_end, + cheddar_nft: self.cheddar_nft.clone(), + total_staked: to_U128s(&self.total_stake), + total_farmed: to_U128s(&self.total_harvested), + fee_rate: self.fee_rate.into(), + accounts_registered: self.accounts_registered, + } + } + + pub fn status(&self, account_id: AccountId) -> Option { + return match self.vaults.get(&account_id) { + Some(mut v) => { + let r = self.current_round(); + v.ping(self.compute_reward_acc(r), r); + // round starts from 1 when now >= farming_start + let r0 = if r > 1 { r - 1 } else { 0 }; + let farmed = self + .farm_token_rates + .iter() + .map(|rate| U128::from(farmed_tokens(v.farmed, *rate))) + .collect(); + return Some(Status { + stake_tokens: v.staked, + stake: v.min_stake.into(), + farmed_units: v.farmed.into(), + farmed_tokens: farmed, + cheddy_nft: v.cheddy, + timestamp: self.farming_start + r0 * ROUND, + }); + } + None => None, + }; + } + // ******************* // + // transaction methods // + // ******************* // + + /// withdraw NFT to a destination account using the `nft_transfer` method. + /// This function is considered safe and will work when contract is paused to allow user + /// to withdraw his NFTs. + #[payable] + pub fn withdraw_boost_nft(&mut self, receiver_id: AccountId) { + assert_one_yocto(); + let user = env::predecessor_account_id(); + let mut vault = self.get_vault(&user); + // TODO - check why is it two account_id using + self._withdraw_cheddy_nft(&user, &mut vault, receiver_id.into()); + self.vaults.insert(&user, &vault); + } + + /// Deposit native near during the setup phase for farming rewards. + /// Panics when the deposit was already done or the setup is completed. + #[payable] + pub fn setup_deposit_near(&mut self) { + self._setup_deposit(&NEAR_TOKEN.parse().unwrap(), env::attached_deposit()) + } + + pub(crate) fn _setup_deposit(&mut self, token: &AccountId, amount: u128) { + assert!( + !self.setup_finalized, + "setup deposits must be done when contract setup is not finalized" + ); + let token_i = find_acc_idx(token, &self.farm_tokens).unwrap(); + let total_rounds = round_number(self.farming_start, self.farming_end, self.farming_end); + let expected = farmed_tokens( + u128::from(total_rounds) * self.farm_unit_emission, + self.farm_token_rates[token_i], + ); + assert_eq!( + self.farm_deposits[token_i], 0, + "deposit already done for the given token" + ); + assert_eq!( + amount, expected, + "Expected deposit for token {} is {}, got {}", + self.farm_tokens[token_i], expected, amount + ); + self.farm_deposits[token_i] = amount; + } + + /// Unstakes given token and transfers it back to the user. + /// If token_id not set - unstake all tokens and close the account + /// NOTE: account once closed must re-register to stake again. + /// Returns vector of staked tokens left (still staked) after the call. + /// Panics if the caller doesn't stake anything or if he doesn't have enough staked tokens. + /// Requires 1 yNEAR payment for wallet 2FA. + #[payable] + pub fn unstake(&mut self, nft_contract_id: &AccountId, token_id: Option) -> Vec { + self.assert_is_active(); + assert_one_yocto(); + let user = env::predecessor_account_id(); + self.internal_nft_unstake(&user, nft_contract_id, token_id).into() + } + + /// Unstakes everything and close the account. Sends all farmed tokens using a ft_transfer + /// and all staked tokens back to the caller. + /// Panics if the caller doesn't stake anything. + /// Requires 1 yNEAR payment for wallet validation. + #[payable] + pub fn close(&mut self) { + self.assert_is_active(); + assert_one_yocto(); + + let account_id = env::predecessor_account_id(); + let mut vault = self.get_vault(&account_id); + self.ping_all(&mut vault); + log!("Closing {} account, farmed: {:?}", &account_id, vault.farmed); + + // if user doesn't stake anything and has no rewards then we can make a shortcut + // and remove the account and return storage deposit. + if vault.is_empty() { + self.vaults.remove(&account_id); + Promise::new(account_id.clone()).transfer(STORAGE_COST); + return; + } + + let units = min_stake(&vault.staked, &self.stake_rates); + self.staked_nft_units -= units; + + // transfer all tokens to user + for nft_contract_id in 0..self.total_stake.len() { + let staked_tokens_ids = &vault.staked[nft_contract_id]; + for token_id in 0..staked_tokens_ids.clone().len() { + self.transfer_staked_nft_token( + account_id.clone(), + nft_contract_id, + staked_tokens_ids[token_id].clone() + ); + } + } + // withdraw farmed to user + self._withdraw_crop(&account_id, vault.farmed); + + if !vault.cheddy.is_empty() { + self._withdraw_cheddy_nft(&account_id, &mut vault, account_id.clone()); + } + + // NOTE: we don't return deposit because it will dramatically complicate logic + // in case we need to recover an account. + self.vaults.remove(&account_id); + } + + /// Withdraws all farmed tokens to the user. It doesn't close the account. + /// Panics if user has not staked anything. + pub fn withdraw_crop(&mut self) { + self.assert_is_active(); + let a = env::predecessor_account_id(); + let mut v = self.get_vault(&a); + self.ping_all(&mut v); + let farmed_units = v.farmed; + v.farmed = 0; + self.vaults.insert(&a, &v); + self._withdraw_crop(&a, farmed_units); + } + + /** transfers harvested tokens to the user + / NOTE: the destination account must be registered on CHEDDAR first! + / NOTE: callers MUST set user `vault.farmed_units` to zero prior to the call + / because in case of failure the callbacks will re-add rewards to the vault */ + fn _withdraw_crop(&mut self, user: &AccountId, farmed_units: u128) { + if farmed_units == 0 { + // nothing to mint nor return. + return; + } + for i in 0..self.farm_tokens.len() { + let amount = farmed_tokens(farmed_units, self.farm_token_rates[i]); + self.transfer_farmed_tokens(user, i, amount); + } + } + + /// Returns the amount of collected fees which are not withdrawn yet. + pub fn get_collected_fee(&self) -> Vec { + to_U128s(&self.fee_collected) + } + + /// Withdraws all collected fee to the treasury. + /// Must make sure treasury is registered + /// Panics if the collected fees == 0. + pub fn withdraw_fees(&mut self) { + log!("Withdrawing collected fee: {:?} tokens", self.fee_collected); + for i in 0..self.stake_nft_tokens.len() { + if self.fee_collected[i] != 0 { + ext_ft::ext(self.stake_nft_tokens[i].clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .ft_transfer( + self.treasury.clone(), + self.fee_collected[i].into(), + Some("fee withdraw".to_string()), + ) + .then(Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_CALLBACK) + .withdraw_fees_callback(i, self.fee_collected[i].into())); + self.fee_collected[i] = 0; + } + } + } + + // ******************* // + // management // + // ******************* // + + /// Opens or closes smart contract operations. When the contract is not active, it won't + /// reject every user call, until it will be open back again. + pub fn set_active(&mut self, is_open: bool) { + self.assert_owner(); + self.is_active = is_open; + } + + /// start and end are unix timestamps (in seconds) + pub fn set_start_end(&mut self, start: u64, end: u64) { + self.assert_owner(); + assert!( + start > env::block_timestamp() / SECOND, + "start must be in the future" + ); + assert!(start < end, "start must be before end"); + self.farming_start = start; + self.farming_end = end; + } + + /// withdraws farming tokens back to owner + pub fn admin_withdraw(&mut self, token: AccountId, amount: U128) { + self.assert_owner(); + // TODO: double check if we want to enable user funds recovery here. + // If not then we need to check if token is in farming_tokens + ext_ft::ext(token.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .ft_transfer( + self.owner_id.clone(), + amount, + Some("admin-withdrawing-back".to_string()) + ); + } + pub fn finalize_setup(&mut self) { + self.assert_owner(); + assert!( + !self.setup_finalized, + "setup deposits must be done when contract setup is not finalized" + ); + let now = env::block_timestamp() / SECOND; + assert!( + now < self.farming_start - ROUND, // TODO: change to 1 day? + "must be finalized at last before farm start" + ); + for i in 0..self.farm_deposits.len() { + assert_ne!( + self.farm_deposits[i], 0, + "Deposit for token {} not done", + self.farm_tokens[i] + ) + } + self.setup_finalized = true; + } + + /// Returns expected and received deposits for farmed tokens + pub fn finalize_setup_expected(&self) -> (Vec, Vec) { + self.assert_owner(); + let total_rounds = u128::from(round_number( + self.farming_start, + self.farming_end, + self.farming_end, + )); + let out = self + .farm_token_rates + .iter() + .map(|rate| farmed_tokens(total_rounds * self.farm_unit_emission, *rate)) + .collect(); + (to_U128s(&out), to_U128s(&self.farm_deposits)) + } + + /***************** + * internal methods */ + + fn assert_is_active(&self) { + assert!(self.setup_finalized, "contract is not setup yet"); + assert!(self.is_active, "contract is not active"); + } + + /// transfers staked NFT tokens (NFT contract identified by an index in + /// self.stake_tokens) back to the user. + /// `self.staked_units` must be adjusted in the caller. The callback will fix the + /// `self.staked_units` if the transfer will fails. + fn transfer_staked_nft_token(&mut self, user: AccountId, nft_contract_i: usize, token_id: TokenId) -> Promise { + // withdraw 1 token. Unimplemented fee + // let fee = amount * self.fee_rate / BASIS_P; + // let amount = amount - fee; + let nft_contract = &self.stake_nft_tokens[nft_contract_i]; + self.total_stake[nft_contract_i] -= 1; + log!("unstaking {} token {}", nft_contract, token_id); + + return ext_nft::ext(nft_contract.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .nft_transfer( + user.clone(), + token_id.clone(), + None, + Some("unstaking".to_string()), + ) + .then(Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_CALLBACK) + .transfer_staked_callback( + user, + nft_contract_i, + token_id.clone().into(), + ) + ) + } + + #[inline] + fn transfer_farmed_tokens(&mut self, u: &AccountId, token_i: usize, amount: u128) -> Promise { + let token = &self.farm_tokens[token_i]; + println!("transfer farmed token: @{} ", token.clone()); + self.total_harvested[token_i] += amount; + + if token == &near() { + return Promise::new(u.clone()).transfer(amount); + } + // OVERFLOW + self.farm_deposits[token_i] -= amount; + let amount: U128 = amount.into(); + + return ext_ft::ext(token.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .ft_transfer(u.clone(), amount, Some("farming".to_string())) + .then(Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_CALLBACK) + .transfer_farmed_callback( + u.clone(), + token_i, + amount + ) + ); + } + + #[private] + pub fn transfer_staked_callback( + &mut self, + user: AccountId, + nft_contract_i: usize, + token_id: TokenId, + //fee: U128, + ) { + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + + PromiseResult::Successful(_) => { + // self.fee_collected[nft_contract_i] += fee.0; + + log!("token withdrawn {}", token_id.clone()); + // we can't remove the vault here, because we don't know if `mint` succeded + // if it didn't succeed, the the mint_callback will try to recover the vault + // and recreate it - so potentially we will send back to the user NEAR deposit + // multiple times. User should call `close` second time to get back + // his NEAR deposit. + } + + PromiseResult::Failed => { + log!( + "transferring token: {} contract: {} failed. Recovering account state", + token_id, + self.stake_nft_tokens[nft_contract_i], + ); + //let full_amount = amount + fee.0; + //self.total_stake[nft_contract_i] += full_amount; + //recover only token, fee is unimplemented now + self.recover_state(&user, true, nft_contract_i, Some(token_id), None); + } + } + } + + #[private] + pub fn transfer_farmed_callback(&mut self, user: AccountId, token_i: usize, amount: U128) { + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => { + // see comment in transfer_staked_callback function + } + + PromiseResult::Failed => { + log!( + "harvesting {} {} token failed. recovering account state", + amount.0, + self.stake_nft_tokens[token_i], + ); + self.recover_state(&user, false, token_i, None, Some(amount.0)); + } + } + } + + #[private] + pub fn withdraw_nft_callback(&mut self, user: AccountId, cheddy: String) { + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => {} + + PromiseResult::Failed => { + log!( + "transferring {} NFT failed. Recovering account state", + cheddy, + ); + let mut v: Vault; + if let Some(v2) = self.vaults.get(&user) { + v = v2; + } else { + // If the vault was closed before by another TX, then we must recover the state + self.accounts_registered += 1; + v = Vault::new(self.stake_nft_tokens.len(), self.reward_acc) + } + v.cheddy = cheddy; + self.vaults.insert(&user, &v); + } + } + } + + #[private] + pub fn withdraw_fees_callback(&mut self, token_i: usize, amount: U128) { + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => {} + + PromiseResult::Failed => { + log!( + "transferring fees {} contract {} failed. Recovering contract state", + amount.0, + self.stake_nft_tokens[token_i], + ); + self.fee_collected[token_i] += amount.0; + } + } + } + + fn recover_state( + &mut self, + user: &AccountId, + is_staked: bool, + contract_i: usize, + token_id: Option, + amount: Option + ) { + let mut v; + if let Some(v2) = self.vaults.get(&user) { + v = v2; + } else { + // If the vault was closed before by another TX, then we must recover the state + self.accounts_registered += 1; + v = Vault::new(self.stake_nft_tokens.len(), self.reward_acc) + } + // NFT contract id + if is_staked { + v.staked[contract_i].push(token_id.unwrap()); + let s = min_stake(&v.staked, &self.stake_rates); + let diff = s - v.min_stake; + if diff > 0 { + self.staked_nft_units += diff; + } + // FT contract id + } else { + self.total_harvested[contract_i] -= amount.unwrap(); + } + + self.vaults.insert(user, &v); + } + + /// Returns the round number since `start`. + /// If now < start return 0. + /// If now == start return 0. + /// if now == start + ROUND return 1... + fn current_round(&self) -> u64 { + round_number( + self.farming_start, + self.farming_end, + env::block_timestamp() / SECOND, + ) + } + + /// creates new empty account. User must deposit tokens using transfer_call + fn create_account(&mut self, user: &AccountId) { + self.vaults + .insert(&user, &Vault::new(self.stake_nft_tokens.len(), self.reward_acc)); + self.accounts_registered += 1; + } + + fn assert_owner(&self) { + assert!( + env::predecessor_account_id() == self.owner_id, + "can only be called by the owner" + ); + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +#[allow(unused_imports)] +mod tests { + use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; + use near_contract_standards::non_fungible_token::core::NonFungibleTokenReceiver; + use near_contract_standards::storage_management::StorageManagement; + use near_sdk::test_utils::{accounts, VMContextBuilder}; + use near_sdk::{testing_env, Balance}; + use near_sdk::MockedBlockchain; + use serde::de::IntoDeserializer; + use std::convert::TryInto; + use std::vec; + + use super::*; + + fn acc_cheddar() -> AccountId { + "cheddar1".to_string().try_into().unwrap() + } + + fn acc_farming2() -> AccountId { + "cheddar2".to_string().try_into().unwrap() + } + + fn acc_staking1() -> AccountId { + "nft1".to_string().try_into().unwrap() + } + + fn acc_staking2() -> AccountId { + "nft2".to_string().try_into().unwrap() + } + + fn acc_nft_cheddy() -> AccountId { + "nft_cheddy".to_string().try_into().unwrap() + } + + fn acc_u1() -> AccountId { + "user1".to_string().try_into().unwrap() + } + + fn acc_u2() -> AccountId { + "user2".to_string().try_into().unwrap() + } + + #[allow(dead_code)] + fn acc_u3() -> AccountId { + "user3".to_string().try_into().unwrap() + } + + fn acc_owner() -> AccountId { + "user_owner".to_string().try_into().unwrap() + } + + /// half of the block round + // const ROUND_NS_H: u64 = ROUND_NS / 2; + /// first and last round + const END: i64 = 10; + const RATE: u128 = E24 * 2; // 2 farming_units / round (60s) + const BOOST: u32 = 250; + + fn round(r: i64) -> u64 { + let r: u64 = (10 + r).try_into().unwrap(); + println!("current round:{} {} ", r, r * ROUND_NS); + return r * ROUND_NS; + } + + /// deposit_dec = size of deposit in e24 to set for the next transacton + fn setup_contract( + predecessor: AccountId, + deposit_dec: u128, + fee_rate: u32, + stake_nft_tokens: Option>, + stake_rates: Option> + ) -> (VMContextBuilder, Contract) { + let mut context = VMContextBuilder::new(); + testing_env!(context.build()); + let contract = Contract::new( + acc_owner(), + stake_nft_tokens.unwrap_or_else(||vec![acc_staking1(), acc_staking2()]), // staking nft tokens + to_U128s(&stake_rates.unwrap_or_else(||vec![E24, E24 / 10])), // staking rates + RATE.into(), // farm_unit_emission + vec![acc_cheddar(), acc_farming2()], // farming tokens + to_U128s(&vec![E24, E24 / 2]), // farming rates + round(0) / SECOND, // farming start + round(END) / SECOND, // farmnig end + acc_nft_cheddy(), // cheddy nft + BOOST, // cheddy boost + fee_rate, + accounts(1), // treasury + ); + contract.check_vectors(); + testing_env!(context + .predecessor_account_id(predecessor.clone()) + .signer_account_id(predecessor.clone()) + .attached_deposit(deposit_dec.into()) + .block_timestamp(round(-10)) + .build()); + (context, contract) + } + + /// epoch is a timer in rounds (rather than miliseconds) + fn stake( + ctx: &mut VMContextBuilder, + ctr: &mut Contract, + user: &AccountId, + nft_token_contract: &AccountId, + token_id: String, + ) { + testing_env!(ctx + .attached_deposit(0) + .predecessor_account_id(nft_token_contract.clone()) + .signer_account_id(user.clone()) + .build()); + ctr.nft_on_transfer(user.clone(), user.clone(), token_id, "to farm".to_string()); + } + + /// epoch is a timer in rounds (rather than miliseconds) + fn unstake( + ctx: &mut VMContextBuilder, + ctr: &mut Contract, + user: &AccountId, + nft_token_contract: &AccountId, + token_id: String, + ) { + testing_env!(ctx + .attached_deposit(1) + .predecessor_account_id(user.clone()) + .build()); + ctr.unstake(nft_token_contract, Some(token_id)); + } + + /// epoch is a timer in rounds (rather than miliseconds) + fn close(ctx: &mut VMContextBuilder, ctr: &mut Contract, user: &AccountId) { + testing_env!(ctx + .attached_deposit(1) + .predecessor_account_id(user.clone()) + .build()); + ctr.close(); + } + + /// epoch is a timer in rounds (rather than miliseconds) + fn register_user_and_stake( + ctx: &mut VMContextBuilder, + ctr: &mut Contract, + user: &AccountId, + nft_contract_id: &AccountId, + stake_token_id: String, + r: i64, + ) { + testing_env!(ctx + .attached_deposit(STORAGE_COST) + .predecessor_account_id(user.clone()) + .signer_account_id(user.clone()) + .block_timestamp(round(r)) + .build()); + + ctr.storage_deposit(None, None); + stake( + ctx, + ctr, + user, + nft_contract_id, + stake_token_id + ); + } + // PASSED + #[test] + fn test_set_active() { + let (_, mut ctr) = setup_contract( + acc_owner(), + 5, + 0, + None, + None + ); + assert_eq!(ctr.is_active, true); + ctr.set_active(false); + assert_eq!(ctr.is_active, false); + } + // PASSED + #[test] + #[should_panic(expected = "can only be called by the owner")] + fn test_set_active_not_admin() { + let (_, mut ctr) = setup_contract( + accounts(0), + 0, + 1, + None, + None + ); + ctr.set_active(false); + } + + fn finalize(ctr: &mut Contract) { + ctr._setup_deposit(&acc_cheddar().into(), 20 * E24); + ctr._setup_deposit(&acc_farming2().into(), 10 * E24); + ctr.finalize_setup(); + } + // PASSED + #[test] + fn test_finalize_setup() { + let (_, mut ctr) = setup_contract( + acc_owner(), + 0, + 0, + None, + None + ); + assert_eq!( + ctr.setup_finalized, false, + "at the beginning setup mut not be finalized" + ); + finalize(&mut ctr); + assert_eq!(ctr.setup_finalized, true) + } + // PASSED + #[test] + #[should_panic(expected = "must be finalized at last before farm start")] + fn test_finalize_setup_too_late() { + let (mut ctx, mut ctr) = setup_contract( + acc_owner(), + 0, + 0, + None, + None + ); + ctr._setup_deposit(&acc_cheddar().into(), 20 * E24); + ctr._setup_deposit(&acc_farming2().into(), 10 * E24); + testing_env!(ctx.block_timestamp(10 * ROUND_NS).build()); + ctr.finalize_setup(); + } + // PASSED + #[test] + #[should_panic(expected = "Expected deposit for token cheddar1 is 20000000000000000000000000")] + fn test_finalize_setup_wrong_deposit() { + let (_, mut ctr) = setup_contract(accounts(1), 0, 0, None, None); + ctr._setup_deposit(&acc_cheddar().into(), 10 * E24); + } + // PASSED + #[test] + #[should_panic(expected = "Deposit for token cheddar2 not done")] + fn test_finalize_setup_not_enough_deposit() { + let (_, mut ctr) = setup_contract(acc_owner(), 0, 0, None, None); + ctr._setup_deposit(&acc_cheddar().into(), 20 * E24); + ctr.finalize_setup(); + } + // PASSED + #[test] + fn test_round_number() { + let (mut ctx, ctr) = setup_contract(acc_u1(), 0, 0, None, None); + assert_eq!(ctr.current_round(), 0); + + assert_eq!(round(-9), ROUND_NS); + assert_eq!(ctr.farming_start, 10 * ROUND); + + testing_env!(ctx.block_timestamp(round(-2)).build()); + assert_eq!(ctr.current_round(), 0); + + testing_env!(ctx.block_timestamp(round(0)).build()); + assert_eq!(ctr.current_round(), 0); + + assert_eq!(round(1), 11 * ROUND_NS); + + testing_env!(ctx.block_timestamp(round(1)).build()); + assert_eq!(ctr.current_round(), 1); + + testing_env!(ctx.block_timestamp(round(10)).build()); + assert_eq!(ctr.current_round(), 10); + testing_env!(ctx.block_timestamp(round(11)).build()); + assert_eq!(ctr.current_round(), 10); + + let total_rounds = round_number(ctr.farming_start, ctr.farming_end, ctr.farming_end); + assert_eq!(total_rounds, 10); + } + + #[test] + #[should_panic( + expected = "The attached deposit is less than the minimum storage balance (60000000000000000000000)" + )] + fn test_min_storage_deposit() { + let (mut ctx, mut ctr) = setup_contract(acc_u1(), 0, 0, None, None); + testing_env!(ctx.attached_deposit(STORAGE_COST / 4).build()); + ctr.storage_deposit(None, None); + } + + #[test] + fn test_storage_deposit() { + let user = acc_u1(); + let (mut ctx, mut ctr) = setup_contract( + user.clone(), + 0, + 0, + None, + None + ); + + match ctr.storage_balance_of(user.clone()) { + Some(_) => panic!("unregistered account must not have a balance"), + _ => {} + }; + + testing_env!(ctx.attached_deposit(STORAGE_COST).build()); + ctr.storage_deposit(None, None); + match ctr.storage_balance_of(user) { + None => panic!("user account should be registered"), + Some(s) => { + assert_eq!(s.available.0, 0, "availabe should be 0"); + assert_eq!( + s.total.0, STORAGE_COST, + "total user storage deposit should be correct" + ); + } + } + } + + #[test] + fn test_staking_nft_unit() { + let user_1 = acc_u1(); + let (mut ctx, mut ctr) = setup_contract( + user_1.clone(), + 0, + 0, + Some(vec![acc_staking1()]), + Some(vec![E24]) + ); + finalize(&mut ctr); + + let user_1_stake_token = "some_token_id".to_string(); + let user_1_stake_contract = acc_staking1(); + register_user_and_stake( + &mut ctx, + &mut ctr, + &user_1, + &user_1_stake_contract, + user_1_stake_token, + 2 // round + ); + + let user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!(user_1_status.stake.0, E24, "u1 should have staked units!"); + } + + #[test] + fn test_alone_staking() { + let user_1 = acc_u1(); + let nft_1 = acc_staking1(); // nft contract 1 + let nft_2 = acc_staking2(); + + let (mut ctx, mut ctr) = setup_contract( + user_1.clone(), + 0, + 0, + None, + None + ); + finalize(&mut ctr); + + assert!( + ctr.status(user_1.clone()).is_none(), + "u1 is not registered yet" + ); + + // register user1 account + testing_env!(ctx.attached_deposit(STORAGE_COST).build()); + ctr.storage_deposit(None, None); + let mut user_1_status = ctr.status(user_1.clone()).unwrap(); + + // NFT contracts as index in staked tokens (contract_i) + for i in 0..user_1_status.stake_tokens.clone().len() { + assert!(&user_1_status.stake_tokens[i].is_empty(), "a1 didn't stake"); + } + assert_eq!(user_1_status.farmed_units.0, 0, "a1 didn't stake no one NFT"); + + // ------------------------------------------------ + // stake before farming_start + testing_env!(ctx.block_timestamp(round(-3)).build()); + stake(&mut ctx, &mut ctr, &user_1, &nft_1, "some_token_id".to_string()); + + user_1_status = ctr.status(user_1.clone()).unwrap(); + let mut user_1_stake: Vec> = vec![vec!["some_token_id".to_string()], vec![]]; + assert_eq!(user_1_status.stake_tokens, user_1_stake, "user1 stake"); + assert_eq!(user_1_status.farmed_units.0, 0, "farming didn't start yet"); + assert_eq!( + ctr.total_stake.len(), user_1_stake.len(), + "total tokens staked should equal to account1 stake." + ); + + // ------------------------------------------------ + // stake one more time before farming_start + testing_env!(ctx.block_timestamp(round(-2)).build()); + stake(&mut ctx, &mut ctr, &user_1, &nft_2, "some_token_id_2".to_string()); + + user_1_status = ctr.status(user_1.clone()).unwrap(); + user_1_stake = vec![vec!["some_token_id".to_string()], vec!["some_token_id_2".to_string()]]; + assert_eq!(user_1_status.stake_tokens, user_1_stake, "user1 stake"); + assert_eq!(user_1_status.farmed_units.0, 0, "farming didn't start yet"); + assert_eq!( + ctr.total_stake.len(), user_1_stake.len(), + "total tokens staked should equal to account1 stake." + ); + + // ------------------------------------------------ + // Staking before the beginning won't yield rewards + testing_env!(ctx.block_timestamp(round(0) - 1).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.stake_tokens, + user_1_stake, + "account1 stake didn't change" + ); + assert_eq!( + user_1_status.farmed_units.0, 0, + "no farmed_units should be rewarded before start" + ); + + // ------------------------------------------------ + // First round - a whole epoch needs to pass first to get first rewards + testing_env!(ctx.block_timestamp(round(0) + 1).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!(user_1_status.farmed_units.0, 0, "need to stake whole round to farm"); + + // ------------------------------------------------ + // 3rd round. We are alone - we should get 100% of emission of first 2 rounds. + + testing_env!(ctx.block_timestamp(round(2)).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.stake_tokens, + user_1_stake, + "account1 stake didn't change" + ); + assert_eq!(user_1_status.farmed_units.0, 2 * RATE, "we take all harvest"); + + // ------------------------------------------------ + // middle of the 3rd round. + // second check in same epoch shouldn't change rewards + testing_env!(ctx.block_timestamp(round(2) + 100).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.farmed_units.0, + 2 * RATE, + "in the same epoch we should harvest only once" + ); + + // ------------------------------------------------ + // last round + testing_env!(ctx.block_timestamp(round(9)).build()); + let total_rounds: u128 = + round_number(ctr.farming_start, ctr.farming_end, ctr.farming_end).into(); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.farmed_units.0, + (total_rounds - 1) * RATE, + "in the last round we should get rewards minus one round" + ); + + // ------------------------------------------------ + // end of farming + testing_env!(ctx.block_timestamp(round(END) + 100).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.farmed_units.0, + total_rounds * RATE, + "after end we should get all rewards" + ); + + testing_env!(ctx.block_timestamp(round(END + 1) + 100).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + let total_farmed = total_rounds * RATE; + assert_eq!( + user_1_status.farmed_units.0, total_farmed, + "after end there is no more farming" + ); + + // ------------------------------------------------ + // withdraw + // ------------------------------------------------ + // Before withdraw farm deposits doesn't changed + assert_eq!(ctr.farm_deposits, vec![20 * E24, 10 * E24]); + + testing_env!(ctx.predecessor_account_id(user_1.clone()).build()); + ctr.withdraw_crop(); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.farmed_units.0, 0, + "after withdrawing we should have 0 farming units" + ); + // After withdraw there is no farm deposits and full of farmed now harvested + assert_eq!(ctr.total_harvested, vec![20 * E24, 10 * E24]); + assert_eq!(ctr.farm_deposits, vec![0,0]); + // stake not changed + assert_eq!(user_1_status.stake_tokens, user_1_stake, "after withdrawing crop stake not changed"); + } + + #[test] + fn test_alone_staking_late() { + let user_1 = acc_u1(); + let nft1 = acc_staking1(); // nft contract 1 + let nft2 = acc_staking2(); + + let (mut ctx, mut ctr) = setup_contract( + user_1.clone(), + 0, + 0, + None, + None + ); + finalize(&mut ctr); + // register user1 account + testing_env!(ctx.attached_deposit(STORAGE_COST).build()); + ctr.storage_deposit(None, None); + + // ------------------------------------------------ + // stake only one token at round 2 + testing_env!(ctx.block_timestamp(round(1)).build()); + stake(&mut ctx, &mut ctr, &user_1, &nft1, "some_token_id_1".to_string()); + + // ------------------------------------------------ + // stake second token in the middle of round 4 + // but firstly verify that we didn't farm anything + testing_env!(ctx.block_timestamp(round(3)).build()); + let mut user_1_status = ctr.status(user_1.clone()).unwrap(); + + let mut user_1_stake:Vec> = vec![vec!["some_token_id_1".to_string()],vec![]]; + assert_eq!(user_1_status.stake_tokens, user_1_stake, "user1 stake"); + assert_eq!(user_1_status.farmed_units.0, 0, "need to stake all tokens to farm"); + + testing_env!(ctx.block_timestamp(round(4) + 500).build()); + stake(&mut ctx, &mut ctr, &user_1, &nft2, "some_token_id_2".to_string()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + user_1_stake = vec![vec!["some_token_id_1".to_string()], vec!["some_token_id_2".to_string()]]; + assert_eq!(user_1_status.stake_tokens, user_1_stake, "user1 stake"); + assert_eq!(user_1_status.farmed_units.0, 0, "full round needs to pass to farm"); + + // ------------------------------------------------ + // at round 6th, after full round of staking we farm the first tokens! + testing_env!(ctx.block_timestamp(round(5)).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!(user_1_status.farmed_units.0, RATE, "full round needs to pass to farm"); + + testing_env!(ctx.block_timestamp(round(END)).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.farmed_units.0, + 6 * RATE, + "farming form round 5 (including) to 10" + ); + } + + #[test] + fn test_staking_2_users() { + let user_1: AccountId = acc_u1(); + let user_2: AccountId = acc_u2(); + let nft1 = acc_staking1(); // nft_contract_id 1 + + let (mut ctx, mut ctr) = setup_contract( + acc_owner(), + 0, + 0, + Some(vec![nft1.clone()]), + Some(vec![E24/10]) + ); + assert_eq!( + ctr.total_stake, [0], + "at the beginning there should be 0 total stake" + ); + finalize(&mut ctr); + + // register user1 account and stake before farming_start + let user_1_stake = vec![vec!["some_token_id_1".to_string(), "some_token_id_1_1".to_string(), "some_token_id_1_2".to_string()]]; + register_user_and_stake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[0].clone(), -2); + stake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[1].clone()); + stake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[2].clone()); + + // ------------------------------------------------ + // at round 4, user2 registers and stakes + // firstly register u2 account (storage_deposit) and then stake. + let user_2_stake = vec![vec!["some_token_id_2".to_string()]]; + register_user_and_stake(&mut ctx, &mut ctr, &user_2, &nft1, user_2_stake.clone()[0].clone()[0].clone(), 3); + + let mut user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.stake_tokens, + user_1_stake, + "account1 stake didn't change" + ); + assert_eq!( + user_1_status.farmed_units.0, + 3 * RATE, + "adding new stake doesn't change current issuance" + ); + assert_eq!(user_1_status.stake.0, 3 * E24 / 10); + + let mut user_2_status = ctr.status(user_2.clone()).unwrap(); + assert_eq!( + user_2_status.stake_tokens, + user_2_stake, + "account2 stake got updated" + ); + assert_eq!(user_2_status.farmed_units.0, 0, "u2 doesn't farm now"); + assert_eq!(user_2_status.stake.0, E24 / 10); + + // ------------------------------------------------ + // 1 epochs later (5th round) user2 should have farming reward + testing_env!(ctx.block_timestamp(round(4)).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.stake_tokens, + user_1_stake, + "account1 stake didn't change" + ); + assert_eq!( + user_1_status.farmed_units.0, + 3 * RATE + RATE * 3 / 4, + "5th round of account1 farming" + ); + + user_2_status = ctr.status(user_2.clone()).unwrap(); + assert_eq!( + user_2_status.stake_tokens, + user_2_stake, + "account1 stake didn't change" + ); + assert_eq!(user_1_status.stake.0, 3 * E24 / 10); + assert_eq!( + user_2_status.farmed_units.0, + RATE / 4, + "account2 first farming is correct" + ); + + // ------------------------------------------------ + // go to the last round of farming, and try to stake - it shouldn't change the rewards. + testing_env!(ctx.block_timestamp(round(END)).build()); + stake(&mut ctx, &mut ctr, &user_2, &nft1,"some_token_id_3".to_string()); + + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!(user_1_status.farmed_units.0, 3 * RATE + RATE * 7 * 3 / 4); + assert_eq!( + user_1_status.farmed_units.0, + 3 * RATE + 7 * RATE * 3 / 4, + "last round of account1 farming" + ); + + user_2_status = ctr.status(user_2.clone()).unwrap(); + let user_2_stake:Vec> = vec![vec!["some_token_id_2".to_string(), "some_token_id_3".to_string()]]; + assert_eq!( + user_2_status.stake_tokens, + user_2_stake, + "account2 stake is updated" + ); + assert_eq!( + user_2_status.farmed_units.0, + 7 * RATE / 4, + "account2 first farming is correct" + ); + + // ------------------------------------------------ + // After farm end farming is disabled + testing_env!(ctx.block_timestamp(round(END + 2)).build()); + + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!(user_1_status.stake.0, 3 * E24 / 10, "account1 stake didn't change"); + assert_eq!( + user_1_status.farmed_units.0, + 3 * RATE + 7 * RATE * 3 / 4, + "last round of account1 farming" + ); + + user_2_status = ctr.status(user_2.clone()).unwrap(); + assert_eq!(user_2_status.stake.0, 2 * E24 / 10, "account2 min stake have been updated "); + assert_eq!( + user_1_status.farmed_units.0, + 3 * RATE + 7 * RATE * 3 / 4, + "but there is no more farming" + ); + } + + #[test] + fn test_stake_unstake() { + let user_1 = acc_u1(); + let user_2 = acc_u2(); + let nft1 = acc_staking1(); // nft contract 1 + let nft2 = acc_staking2(); + + let (mut ctx, mut ctr) = setup_contract( + user_1.clone(), + 0, + 0, + None, + None + ); + finalize(&mut ctr); + + // ----------------------------------------------- + // register and stake by user1 and user2 - both will stake the same amounts + let user_1_stake = vec![vec!["some_token_id_1".to_string()], vec!["some_token_id_2".to_string(), "some_token_id_2_2".to_string()]]; + let user_2_stake = vec![vec!["some_token_id_3".to_string()], vec!["some_token_id_4".to_string(), "some_token_id_4_2".to_string()]]; + + // user_stake structure explanation + + // [ [token_i, token_i, token_i...], [token_i, token_i, token_i...],... [token_i, token_i, token_i...] ] + // ^------nft_contract_j--------^ ^------nft_contract_j--------^ ^------nft_contract_j--------^ + + // both users stake same: + // - one token from nft_contract_1 + // - two tokens from nft_contract_2 + // register users and stake 1 token from nft_contract_1 + // "some_token_id_1" from user1 and "some_token_id_3" from user2 + register_user_and_stake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[0].clone() , -2); + register_user_and_stake(&mut ctx, &mut ctr, &user_2, &nft1, user_2_stake.clone()[0].clone()[0].clone() , -2); + // stake more from both users + stake(&mut ctx, &mut ctr, &user_1, &nft2, user_1_stake.clone()[1].clone()[0].clone()); + stake(&mut ctx, &mut ctr, &user_1, &nft2, user_1_stake.clone()[1].clone()[1].clone()); + + stake(&mut ctx, &mut ctr, &user_2, &nft2, user_2_stake.clone()[1].clone()[0].clone()); + stake(&mut ctx, &mut ctr, &user_2, &nft2, user_2_stake.clone()[1].clone()[1].clone()); + + assert_eq!(ctr.total_stake[0], 2 as u128, "token1 stake two NFT tokens for contract nft1 (index = 0"); + assert_eq!(ctr.total_stake[1], 4 as u128, "token1 stake four NFT tokens for contract nft2 (index = 1"); + + // user1 unstake at round 5 + testing_env!(ctx.block_timestamp(round(4)).build()); + unstake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[0].clone()); + let user_1_status = ctr.status(user_1.clone()).unwrap(); + let user_2_status = ctr.status(user_2.clone()).unwrap(); + + assert_eq!(ctr.total_stake[0], 1 as u128, "token1 stake was reduced"); + assert_eq!(ctr.total_stake[1], 4 as u128, "token2 stake is same"); + + assert_eq!( + user_1_status.farmed_units.0, + 4 / 2 * RATE, + "user1 and user2 should farm equally in first 4 rounds" + ); + assert_eq!( + user_2_status.farmed_units.0, + 4 / 2 * RATE, + "user1 and user2 should farm equally in first 4 rounds" + ); + + // check at round 7 - user1 should not farm any more + testing_env!(ctx.block_timestamp(round(6)).build()); + let user_1_status = ctr.status(user_1.clone()).unwrap(); + let user_2_status = ctr.status(user_2.clone()).unwrap(); + + assert_eq!( + user_1_status.farmed_units.0, + 4 / 2 * RATE, + "user1 doesn't farm any more" + ); + assert_eq!( + user_2_status.farmed_units.0, + (4 / 2 + 2) * RATE, + "user2 gets 100% of farming" + ); + + // unstake other tokens + unstake(&mut ctx, &mut ctr, &user_2, &nft1, user_2_stake.clone()[0].clone()[0].clone()); + unstake(&mut ctx, &mut ctr, &user_1, &nft2, user_1_stake.clone()[1].clone()[0].clone()); + unstake(&mut ctx, &mut ctr, &user_1, &nft2, user_1_stake.clone()[1].clone()[1].clone()); + + println!("user_1 status {:?}", ctr.status(user_1.clone())); + assert_eq!(ctr.total_stake[0], 0, "token1 stake was reduced"); + assert_eq!(ctr.total_stake[1], 2, "token2 is reduced"); + assert!( + ctr.status(user_1.clone()).is_none(), + "user1 should be removed when unstaking everything" + ); + + // close accounts + testing_env!(ctx.block_timestamp(round(7)).build()); + close(&mut ctx, &mut ctr, &user_2); + assert_eq!(ctr.total_stake[0], 0, "token1"); + assert_eq!(ctr.total_stake[1], 0, "token2"); + assert!( + ctr.status(user_2.clone()).is_none(), + "u1 should be removed when unstaking everything" + ); + } + + #[test] + fn test_nft_boost() { + let user_1: AccountId = acc_u1(); + let user_2: AccountId = acc_u2(); + let nft1: AccountId = acc_staking1(); + + let (mut ctx, mut ctr) = setup_contract( + acc_owner(), + 0, + 0, + Some(vec![nft1.clone()]), + Some(vec![E24]), + ); + finalize(&mut ctr); + + // ------------------------------------------------ + // register and stake by user1 and user2 - both will stake the same amounts, + // but user1 will have nft boost + + let user_1_stake:Vec> = vec![vec!["some_token".to_string()]]; + register_user_and_stake(&mut ctx, &mut ctr, &user_1, &nft1, user_1_stake.clone()[0].clone()[0].clone(), -2); + + testing_env!(ctx.predecessor_account_id(acc_nft_cheddy()).build()); + + ctr.nft_on_transfer(user_1.clone(), user_1.clone(), "1".into(), "cheddy".into()); + + register_user_and_stake(&mut ctx, &mut ctr, &user_2, &nft1, "some_token_2".into(), -2); + + // check at round 3 + testing_env!(ctx.block_timestamp(round(2)).build()); + let user_1_status = ctr.status(user_1.clone()).unwrap(); + let user_2_status = ctr.status(user_2.clone()).unwrap(); + + assert!( + user_1_status.farmed_units.0 > 2 / 2 * RATE, + "user1 should farm more than the 'normal' rate" + ); + assert!( + user_2_status.farmed_units.0 < 2 / 2 * RATE, + "user2 should farm less than the 'normal' rate" + ); + + // withdraw nft during round 3 + testing_env!(ctx + .predecessor_account_id(user_1.clone()) + .block_timestamp(round(2) + 1000) + .attached_deposit(1) + .build()); + ctr.withdraw_boost_nft(user_1.clone()); + + // check at round 4 - user1 should farm at equal rate as user2 + testing_env!(ctx.block_timestamp(round(3)).build()); + let user_1_status_r4 = ctr.status(user_1.clone()).unwrap(); + let user_2_status_r4 = ctr.status(user_2.clone()).unwrap(); + + assert_eq!( + user_1_status_r4.farmed_units.0 - user_1_status.farmed_units.0, + RATE / 2, + "user1 farming rate is equal to user2" + ); + assert_eq!( + user_2_status_r4.farmed_units.0 - user_2_status.farmed_units.0, + RATE / 2, + "user1 farming rate is equal to user2", + ); + } + #[test] + fn test_stake_by_token_id_untake_all() { + let user_1: AccountId = acc_u1(); + let nft1: AccountId = acc_staking1(); + + let (mut ctx, mut ctr) = setup_contract( + acc_owner(), + 0, + 0, + Some(vec![nft1.clone()]), + Some(vec![E24/20]), + ); + finalize(&mut ctr); + + let user_1_stake:Vec> = vec![vec!["some_token".to_string(),"some_token_2".to_string()]]; + + register_user_and_stake(&mut ctx, &mut ctr, &user_1, &nft1, "some_token".to_string(), -2); + stake(&mut ctx, &mut ctr, &user_1, &nft1, "some_token_2".to_string()); + let mut user_1_status = ctr.status(user_1.clone()).unwrap(); + + assert_eq!(user_1_status.stake_tokens, user_1_stake, "stake tokens as ids must be equal to vector"); + assert_eq!(user_1_status.farmed_units.0, 0, "no farmed units before before round 0"); + + // ------------------------------------------------ + // 1 epochs later (5th round) user1 should have farming reward + testing_env!(ctx.block_timestamp(round(4)).build()); + user_1_status = ctr.status(user_1.clone()).unwrap(); + assert_eq!( + user_1_status.stake_tokens, + user_1_stake, + "user stake didn't change" + ); + println!("{:?} ", user_1_status); + assert_eq!( + user_1_status.farmed_units.0, + 4 * RATE, + "farmed units" + ); + assert_eq!( + user_1_status.farmed_tokens, + [U128::from(8 * E24), U128::from(4 * E24)], + "farmed tokens" + ); + assert_eq!( + user_1_status.stake.0, + 2 * E24 / 20, + "farmed tokens" + ); + + // unstake all - no token_id declared - go to self.close() + testing_env!(ctx + .attached_deposit(1) + .predecessor_account_id(user_1.clone()) + .build()); + ctr.unstake(&nft1, None); + assert!( + ctr.status(user_1.clone()).is_none(), + "account closed" + ); + assert_eq!(ctr.total_stake[0], 0, "token1 stake was reduced"); + } +} diff --git a/p3-farm-nft/src/storage_management.rs b/p3-farm-nft/src/storage_management.rs new file mode 100644 index 0000000..125269c --- /dev/null +++ b/p3-farm-nft/src/storage_management.rs @@ -0,0 +1,89 @@ +use near_contract_standards::{ + storage_management::StorageBalance, + storage_management::StorageBalanceBounds, + storage_management::StorageManagement +}; + +use crate::*; + +#[derive(BorshStorageKey, BorshSerialize)] +pub enum StorageKeys { + WhitelistedNFTTokens +} + +#[near_bindgen] +impl StorageManagement for Contract { + /// Registers a new account + #[allow(unused_variables)] + #[payable] + fn storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + assert!(self.is_active, "contract is not active"); + let amount: Balance = env::attached_deposit(); + let account_id = account_id + .unwrap_or_else(|| env::predecessor_account_id()); + if self.vaults.contains_key(&account_id) { + log!("The account is already registered, refunding the deposit"); + if amount > 0 { + Promise::new(env::predecessor_account_id()).transfer(amount); + } + } else { + assert!( + amount >= STORAGE_COST, + "The attached deposit is less than the minimum storage balance ({})", + STORAGE_COST + ); + self.create_account(&account_id); + + let refund = amount - STORAGE_COST; + if refund > 0 { + Promise::new(env::predecessor_account_id()).transfer(refund); + } + } + storage_balance() + } + + /// Method not supported. Close the account (`close()` or + /// `storage_unregister(true)`) to close the account and withdraw deposited NEAR. + #[allow(unused_variables)] + fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + panic!("Storage withdraw not possible, close the account instead"); + } + + /// When force == true it will close the account. Otherwise this is noop. + fn storage_unregister(&mut self, force: Option) -> bool { + if Some(true) == force { + self.close(); + return true; + } + false + } + + /// Mix and min balance is always MIN_BALANCE. + fn storage_balance_bounds(&self) -> StorageBalanceBounds { + StorageBalanceBounds { + min: STORAGE_COST.into(), + max: Some(STORAGE_COST.into()), + } + } + + /// If the account is registered the total and available balance is always MIN_BALANCE. + /// Otherwise None. + fn storage_balance_of(&self, account_id: AccountId) -> Option { + let account_id: AccountId = account_id.into(); + if self.vaults.contains_key(&account_id) { + return Some(storage_balance()); + } + None + } +} + +fn storage_balance() -> StorageBalance { + StorageBalance { + total: STORAGE_COST.into(), + available: U128::from(0), + } +} diff --git a/p3-farm-nft/src/token_standards.rs b/p3-farm-nft/src/token_standards.rs new file mode 100644 index 0000000..5d8fd95 --- /dev/null +++ b/p3-farm-nft/src/token_standards.rs @@ -0,0 +1,140 @@ +use crate::*; + +use near_contract_standards::{ + non_fungible_token::core::NonFungibleTokenReceiver, + non_fungible_token::TokenId, + fungible_token::receiver::FungibleTokenReceiver, +}; + +/// NFT Receiver message switcher. +/// Points to which transfer option is choosed for +enum TransferInstruction { + ToFarm, + ToCheddyBoost, + Unknown +} + +impl From for TransferInstruction { + fn from(msg: String) -> Self { + match &msg[..] { + "to farm" => TransferInstruction::ToFarm, + "cheddy" => TransferInstruction::ToCheddyBoost, + _ => TransferInstruction::Unknown + } + } +} + +/// NFT Receiver +/// Used when an NFT is transferred using `nft_transfer_call`. +/// This function is considered safe and will work when contract is paused to allow user +/// to accumulate bonuses. +/// Message from transfer switch options: +/// - NFT transfer to Farm +/// - Cheddy NFT transfer for rewards boost +#[near_bindgen] +impl NonFungibleTokenReceiver for Contract { + fn nft_on_transfer( + &mut self, + sender_id: AccountId, + previous_owner_id: AccountId, + token_id: TokenId, + msg: String, + ) -> PromiseOrValue { + let nft_contract_id:NftContractId = env::predecessor_account_id(); + assert_ne!( + nft_contract_id, env::signer_account_id(), + "ERR_NOT_CROSS_CONTRACT_CALL" + ); + assert_eq!( + previous_owner_id, env::signer_account_id(), + "ERR_OWNER_NOT_SIGNER" + ); + + match TransferInstruction::from(msg) { + // "cheddy" message for transfer P3 boost + TransferInstruction::ToCheddyBoost => { + if env::predecessor_account_id() != self.cheddar_nft { + log!("Only Cheddy NFTs ({}) are supported", self.cheddar_nft); + return PromiseOrValue::Value(true) + } + let v = self.vaults.get(&previous_owner_id); + if v.is_none() { + log!("Account not registered. Register prior to depositing NFT"); + return PromiseOrValue::Value(true) + } + let mut v = v.unwrap(); + if !v.cheddy.is_empty() { + log!("Account already has Cheddy deposited. You can only deposit one cheddy"); + return PromiseOrValue::Value(true) + } + log!("Staking Cheddy NFT - you will obtain a special farming boost"); + self.ping_all(&mut v); + + v.cheddy = token_id; + self._recompute_stake(&mut v); + self.vaults.insert(&previous_owner_id, &v); + return PromiseOrValue::Value(false) + }, + // "to farm" message for transfer NFT into P3 to stake + TransferInstruction::ToFarm => { + self.assert_is_active(); + // TODO - push it to internal nft stake + // stake + let stake_result = self.internal_nft_stake(&previous_owner_id, &nft_contract_id, token_id); + if !stake_result { + return PromiseOrValue::Value(true) + } + return PromiseOrValue::Value(false) + } + // unknown message (or no message) - we are refund + TransferInstruction::Unknown => { + log!("ERR_UNKNOWN_MESSAGE"); + return PromiseOrValue::Value(true) + } + } + } +} + +/// FT Receiver +/// token deposits are done through NEP-141 ft_transfer_call to the NEARswap contract. +#[near_bindgen] +impl FungibleTokenReceiver for Contract { + /** + FungibleTokenReceiver implementation Callback on receiving tokens by this contract. + Handles both farm deposits and stake deposits. For farm deposit (sending tokens + to setup the farm) you must set "setup reward deposit" msg. + Otherwise tokens will be staken. + Returns zero. + Panics when: + - account is not registered + - or receiving a wrong token + - or making a farm deposit after farm is finalized + - or staking before farm is finalized. */ + #[allow(unused_variables)] + fn ft_on_transfer( + &mut self, + sender_id: AccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue { + let ft_token_id = env::predecessor_account_id(); + assert!( + ft_token_id != near(), + "near must be sent using deposit_near()" + ); + assert!(amount.0 > 0, "deposited amount must be positive"); + if msg == "setup reward deposit" { + self._setup_deposit(&ft_token_id, amount.0); + } else { + log!( + "Contract accept only NFT farming and staking! Refund transfer from @{} with token {} amount {}", + sender_id, + ft_token_id, + amount.0 + ); + return PromiseOrValue::Value(amount) + } + + return PromiseOrValue::Value(U128(0)) + } +} diff --git a/p3-farm-nft/src/vault.rs b/p3-farm-nft/src/vault.rs new file mode 100644 index 0000000..b9c999b --- /dev/null +++ b/p3-farm-nft/src/vault.rs @@ -0,0 +1,211 @@ +//! Vault is information per user about their balances in the exchange. +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::Serialize; + +use near_sdk::{env, log, AccountId, Balance}; + +use crate::*; + +pub (crate) type TokenIds = Vec; +#[derive(Debug, BorshSerialize, BorshDeserialize, Serialize)] +#[cfg_attr(feature = "test", derive(Default, Debug, Clone))] +pub struct Vault { + /// Contract.reward_acc value when the last ping was called and rewards calculated + pub reward_acc: Balance, + /// Staking tokens locked in this vault + /// index - contract id + /// value - token ids - [] + pub staked: Vec, + pub min_stake: Balance, + /// Amount of accumulated, not withdrawn farmed units. When withdrawing the + /// farmed units are translated to all `Contract.farm_tokens` based on + /// `Contract.farm_token_rates` + pub farmed: Balance, + /// Cheddy NFT deposited to get an extra boost. Only one Cheddy can be deposited to a + /// single acocunt. + pub cheddy: TokenId, +} + +impl Vault { + pub fn new(staked_len: usize, reward_acc: Balance) -> Self { + Self { + reward_acc, + staked: vec![TokenIds::new(); staked_len], + min_stake: 0, + farmed: 0, + cheddy: "".into(), + } + } + + /** + Update rewards for locked tokens in past epochs + Arguments: + - `reward_acc`: Contract.reward_acc value + - `round`: current round + */ + pub fn ping(&mut self, reward_acc: Balance, round: u64) { + // note: the last round is at self.farming_end + // if farming didn't start, ignore the rewards update + if round == 0 { + return; + } + // no new rewards + if self.reward_acc >= reward_acc { + return; // self.farmed; + } + self.farmed += self.min_stake * (reward_acc - self.reward_acc) / ACC_OVERFLOW; + self.reward_acc = reward_acc; + } + // If all vault's units is empty returns true + #[inline] + pub fn is_empty(&self) -> bool { + all_zeros(&self.staked) && self.farmed == 0 && self.cheddy.is_empty() + } +} + +impl Contract { + /// Returns the registered vault. + /// Panics if the account is not registered. + #[inline] + pub(crate) fn get_vault(&self, account_id: &AccountId) -> Vault { + self.vaults.get(account_id).expect(ERR10_NO_ACCOUNT) + } + + pub(crate) fn ping_all(&mut self, v: &mut Vault) { + let r = self.current_round(); + self.update_reward_acc(r); + v.ping(self.reward_acc, r); + } + + /// updates the rewards accumulator + pub(crate) fn update_reward_acc(&mut self, round: u64) { + let new_acc = self.compute_reward_acc(round); + // we should advance with rounds if self.t is zero, otherwise we have a jump and + // don't compute properly the accumulator. + if self.staked_nft_units == 0 || new_acc != self.reward_acc { + self.reward_acc = new_acc; + self.reward_acc_round = round; + } + } + + /// computes the rewards accumulator. + /// NOTE: the current, optimized algorithm will not farm anything if + /// `self.rate * ACC_OVERFLOW / self.t < 1` + pub(crate) fn compute_reward_acc(&self, round: u64) -> u128 { + // covers also when round == 0 + if self.reward_acc_round == round || self.staked_nft_units == 0 { + return self.reward_acc; + } + + self.reward_acc + + u128::from(round - self.reward_acc_round) * self.farm_unit_emission * ACC_OVERFLOW + / u128::from(self.staked_nft_units) + } + + pub(crate) fn _recompute_stake(&mut self, v: &mut Vault) { + let mut s = min_stake(&v.staked, &self.stake_rates); + + if !v.cheddy.is_empty() { + s += s * u128::from(self.cheddar_nft_boost) / BASIS_P; + } + + if s > v.min_stake { + let diff = s - v.min_stake; + self.staked_nft_units += diff; // must be called after ping_s + v.min_stake = s; + } else if s < v.min_stake { + let diff = v.min_stake - s; + self.staked_nft_units -= diff; // must be called after ping_s + v.min_stake = s; + } + } + + /// Returns new stake units + pub(crate) fn internal_nft_stake( + &mut self, + previous_owner_id: &AccountId, + nft_contract_id: &NftContractId, + token: TokenId, + ) -> bool { + // find index for staking token into Contract.stake_tokens + if let Some(nft_contract_i) = find_acc_idx(&nft_contract_id, &self.stake_nft_tokens) { + let mut v = self.get_vault(previous_owner_id); + + // firstly update the past rewards + self.ping_all(&mut v); + // after that add "token" to staked into vault + v.staked[nft_contract_i].push(token.clone()); + // update total staked info about this token + self.total_stake[nft_contract_i] += 1; + + self._recompute_stake(&mut v); + self.vaults.insert(previous_owner_id, &v); + log!("Staked @{} from {}, stake_unit: {}", token.clone(), nft_contract_id, v.min_stake); + + return true + } else { + return false + } + } + + /// Returns remaining tokens user has staked after the unstake. + /// If token not declared - unstake all tokens for this nft_contract + pub(crate) fn internal_nft_unstake( + &mut self, + receiver_id: &AccountId, + nft_contract_id: &AccountId, + token: Option, + ) -> Vec { + let nft_contract_i = find_acc_idx(nft_contract_id, &self.stake_nft_tokens).unwrap(); + let mut v = self.get_vault(receiver_id); + + if let Some(token_id) = token { + assert!(v.staked[nft_contract_i].contains(&token_id), "{}", ERR30_NOT_ENOUGH_STAKE); + self.ping_all(&mut v); + + let token_i = find_token_idx(&token_id, &v.staked[nft_contract_i]).unwrap(); + let removed_token_id = v.staked[nft_contract_i].remove(token_i); + + let remaining_tokens = v.staked[nft_contract_i].clone(); + + self._recompute_stake(&mut v); + + // check if we are withdraw all staked tokens for all nft contracts + if all_zeros(&v.staked) { + self.close(); + return vec![]; + } + self.vaults.insert(receiver_id, &v); + self.transfer_staked_nft_token(receiver_id.clone(), nft_contract_i, removed_token_id); + return remaining_tokens; + } else { + self.close(); + return vec![]; + } + } + + pub(crate) fn _withdraw_cheddy_nft(&mut self, user: &AccountId, v: &mut Vault, receiver: AccountId) { + assert!(!v.cheddy.is_empty(), "Sender has no NFT deposit"); + self.ping_all(v); + + ext_nft::ext(self.cheddar_nft.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .nft_transfer( + receiver, + v.cheddy.clone(), + None, + Some("Cheddy withdraw".to_string()) + ) + .then( Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_CALLBACK) + .withdraw_nft_callback( + user.clone(), + v.cheddy.clone() + ) + ); + + v.cheddy = "".into(); + self._recompute_stake(v); + } +} diff --git a/p4-pool/Cargo.toml b/p4-pool/Cargo.toml new file mode 100644 index 0000000..7f73f87 --- /dev/null +++ b/p4-pool/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "p4-pool" +version = "0.1.0" +authors = ["Guacharo"] +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +uint = { version = "0.9.0", default-features = false } +near-sdk = "4.0.0" +near-contract-standards = "4.0.0" + +[dev-dependencies] +# near-primitives = { git = "https://github.com/nearprotocol/nearcore.git" } +# near-sdk-sim = { git = "https://github.com/near/near-sdk-rs.git", version="v3.1.0" } diff --git a/xcheddar/Cargo.toml b/xcheddar/Cargo.toml new file mode 100644 index 0000000..eea3f7a --- /dev/null +++ b/xcheddar/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "xcheddar-token" +version = "1.0.2" +authors = ["Guacharo"] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +uint = { version = "0.9.0", default-features = false } +near-sys = "0.1.0" +near-contract-standards = "4.0.0" +near-sdk = "4.0.0" +chrono = "0.4.19" + +[dev-dependencies] +cheddar-coin = {path = "../cheddar"} \ No newline at end of file diff --git a/xcheddar/README.md b/xcheddar/README.md new file mode 100644 index 0000000..607bc8f --- /dev/null +++ b/xcheddar/README.md @@ -0,0 +1,166 @@ +# XCheddar Token Contract + +### Sumary +* Stake CHEDDAR token to lock in the contract and get XCHEDDAR on price P, +XCHEDDAR_amount = staked_CHEDDAR / P_staked, +where P_staked = locked_CHEDDAR_token_amount / XCHEDDAR_total_supply. + +* Redeem CHEDDAR by unstake using XCHEDDAR token on price P, +redeemed_CHEDDAR = unstaked_XCHEDDAR * P_unstaked, +where P_unstaked = locked_CHEDDAR_token_amount / XCHEDDAR_total_supply. + +* Anyone can add CHEDDAR as reward for those locked CHEDDAR users. +locked_CHEDDAR_token amount would increase `reward_per_month` after `reward_genesis_time_in_sec`. + +* Owner can modify `reward_genesis_time_in_sec` before it passed. + +* Owner can modify `reward_per_month`. + +### Compiling + +You can build release version by running next scripts inside each contract folder: + +``` +cd xcheddar +rustup target add wasm32-unknown-unknown +RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release +cp target/wasm32-unknown-unknown/release/xcheddar_token.wasm xcheddar/res/xcheddar_token.wasm +``` + +#### Also build cheddar contract which you can find in ./cheddar folder: +``` +cd cheddar +rustup target add wasm32-unknown-unknown +RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release +cp target/wasm32-unknown-unknown/release/cheddar_coin.wasm xcheddar/res/cheddar_coin.wasm +``` + +### Deploying to TestNet (export $XCHEDDAR_TOKEN id before) + +To deploy to TestNet, you can use next command: +```bash +near deploy -f --wasmFile target/wasm32-unknown-unknown/release/xcheddar_token.wasm --accountId $XCHEDDAR_TOKEN +#dev-deploy +near dev-deploy -f --wasmFile target/wasm32-unknown-unknown/release/xcheddar_token.wasm +``` + +This will output on the contract ID it deployed. + +### Contract Metadata +```rust +pub struct ContractMetadata { + pub version: String, + pub owner_id: AccountId, + /// backend locked token id + pub locked_token: AccountId, + /// at prev_distribution_time, reward token that haven't distribute yet + pub undistributed_reward: U128, + /// at prev_distribution_time, backend staked token amount + pub locked_token_amount: U128, + // at call time, the amount of undistributed reward + pub cur_undistributed_reward: U128, + // at call time, the amount of backend staked token + pub cur_locked_token_amount: U128, + /// XCHEDDAR token supply + pub supply: U128, + /// previous reward distribution time in secs + pub prev_distribution_time_in_sec: u32, + /// reward start distribution time in secs + pub reward_genesis_time_in_sec: u32, + /// reward token amount per 30-day period + pub reward_per_month: U128, + /// XCHEDDAR holders account number + pub account_number: u64, +} +``` + +### FT Metadata +```rust +FungibleTokenMetadata { + spec: FT_METADATA_SPEC.to_string(), + name: String::from("XCheddar Finance Token"), + symbol: String::from("XCHEDDAR"), + // see code for the detailed icon content + icon: Some(String::from("...")), + cheddarerence: None, + cheddarerence_hash: None, + decimals: 24, +} +``` + +### Initialize +fill with your accounts for token contract, owner and default user for tests + +```shell +export CHEDDAR_TOKEN=token-v3.cheddar.testnet +export XCHEDDAR_TOKEN= +export XCHEDDAR_OWNER= +export USER_ACCOUNT= +export GAS=100000000000000 +export HUNDRED_CHEDDAR=100000000000000000000000000 +# 0.01 +export TEN_MILLI_CHEDDAR=10000000000000000000000 +export ONE_CHEDDAR=1000000000000000000000000 +export FIVE_CHEDDAR=5000000000000000000000000 +export TEN_CHEDDAR=10000000000000000000000000 + +near call $XCHEDDAR_TOKEN new '{"owner_id": "'$XCHEDDAR_OWNER'", "locked_token": "'$CHEDDAR_TOKEN'"}' --account_id=$XCHEDDAR_TOKEN +``` +Note: It would set the reward genesis time into 30 days from then on. + +### Usage + +#### view functions +```bash +# contract metadata gives contract details +near view $XCHEDDAR_TOKEN contract_metadata +# converted timestamps to UTC Datetime and converted from yocto to tokens amounts +near view $XCHEDDAR_TOKEN contract_metadata_human_readable +# get the CHEDDAR / X-CHEDDAR price in 1e8 +near view $XCHEDDAR_TOKEN get_virtual_price + +# ************* from NEP-141 ************* +# see user if registered +near view $XCHEDDAR_TOKEN storage_balance_of '{"account_id": "'$USER_ACCOUNT'"}' +# token metadata +near view $XCHEDDAR_TOKEN ft_metadata +# user token balance +near view $XCHEDDAR_TOKEN ft_balance_of '{"account_id": "'$USER_ACCOUNT'"}' +``` + +#### register +from NEP-141. +```bash +near view $XCHEDDAR_TOKEN storage_balance_of '{"account_id": "'$USER_ACCOUNT'"}' +near call $XCHEDDAR_TOKEN storage_deposit '{"account_id": "'$USER_ACCOUNT'", "registration_only": true}' --account_id=$USER_ACCOUNT --amount=0.1 +# register XCHEDDAR in CHEDDAR contract +near call $CHEDDAR_TOKEN storage_deposit '' --account_id=$XCHEDDAR_TOKEN --amount=0.1 +``` + +#### stake 100 CHEDDAR to get XCHEDDAR +```bash +near call $CHEDDAR_TOKEN ft_transfer_call '{"receiver_id": "'$XCHEDDAR_TOKEN'", "amount": "'$HUNDRED_CHEDDAR'", "msg": ""}' --account_id=$USER_ACCOUNT --depositYocto=1 --gas=$GAS +``` + +#### add 100 CHEDDAR as a reward +```bash +near call $CHEDDAR_TOKEN ft_transfer_call '{"receiver_id": "'$XCHEDDAR_TOKEN'", "amount": "'$HUNDRED_CHEDDAR'", "msg": "reward"}' --account_id=$XCHEDDAR_OWNER --depositYocto=1 --gas=$GAS +``` + +#### owner reset reward genesis time +```bash +near call $XCHEDDAR_TOKEN get_owner '' --account_id=$XCHEDDAR_OWNER +near call $XCHEDDAR_TOKEN reset_reward_genesis_time_in_sec '{"reward_genesis_time_in_sec": 1656578165}' --account_id=$XCHEDDAR_OWNER +``` +Note: would return false if already past old genesis time or the new genesis time is a past time. + +#### owner modify reward_per_second to 5 CHEDDAR +```bash +near call $XCHEDDAR_TOKEN set_reward_per_second '{"reward_per_second": "'$TEN_MILLI_CHEDDAR'", "distribute_before_change": true}' --account_id=$XCHEDDAR_OWNER --gas=$GAS +``` +Note: If `distribute_before_change` is true, contract will sync up reward distribution using the old `reward_per_second` at call time before changing to the new one. + +#### unstake 10 XCHEDDAR get CHEDDAR and reward back +```bash +near call $XCHEDDAR_TOKEN unstake '{"amount": "'$TEN_CHEDDAR'"}' --account_id=$USER_ACCOUNT --depositYocto=1 --gas=$GAS +``` \ No newline at end of file diff --git a/xcheddar/res/cheddar_coin.wasm b/xcheddar/res/cheddar_coin.wasm new file mode 100755 index 0000000..6b89263 Binary files /dev/null and b/xcheddar/res/cheddar_coin.wasm differ diff --git a/xcheddar/res/xcheddar_token.wasm b/xcheddar/res/xcheddar_token.wasm new file mode 100755 index 0000000..68729d2 Binary files /dev/null and b/xcheddar/res/xcheddar_token.wasm differ diff --git a/xcheddar/src/lib.rs b/xcheddar/src/lib.rs new file mode 100644 index 0000000..e5ee924 --- /dev/null +++ b/xcheddar/src/lib.rs @@ -0,0 +1,92 @@ +use near_contract_standards::fungible_token::metadata::{ + FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC, +}; +use near_contract_standards::fungible_token::FungibleToken; + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LazyOption; +use near_sdk::json_types::U128; +use near_sdk::{env, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue}; + +use crate::utils::*; +pub use crate::views::ContractMetadata; + +mod xcheddar; +mod utils; +mod owner; +mod views; +mod storage_impl; + +#[ext_contract(ext_cheddar)] +pub trait ExtCheddar { + fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option); +} + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct Contract { + pub ft: FungibleToken, + pub owner_id: AccountId, + pub locked_token: AccountId, + /// deposit reward that does not distribute to locked Cheddar yet + pub undistributed_reward: Balance, + /// locked amount + pub locked_token_amount: Balance, + /// the previous distribution time in seconds + pub prev_distribution_time_in_sec: u32, + /// when would the reward starts to distribute + pub reward_genesis_time_in_sec: u32, + /// reward per second. Distributed into locked_amount reward = time_diff * reward_per_second + /// where time_diff = cur_timestamp - prev_distribution_time_in_sec + pub reward_per_second: Balance, + /// current account number in contract + pub account_number: u64, +} + +#[near_bindgen] +impl Contract { + #[init] + //Initialize with setting the reward genesis time into 30 days from init time + + pub fn new(owner_id: AccountId, locked_token: AccountId) -> Self { + let initial_reward_genisis_time = DURATION_30DAYS_IN_SEC + nano_to_sec(env::block_timestamp()); + Contract { + ft: FungibleToken::new(b"a".to_vec()), + owner_id: owner_id.into(), + locked_token: locked_token.into(), + undistributed_reward: 0, + locked_token_amount: 0, + prev_distribution_time_in_sec: initial_reward_genisis_time, + reward_genesis_time_in_sec: initial_reward_genisis_time, + reward_per_second: 0, + account_number: 0, + } + } +} + +near_contract_standards::impl_fungible_token_core!(Contract, ft); + +#[near_bindgen] +impl FungibleTokenMetadataProvider for Contract { + fn ft_metadata(&self) -> FungibleTokenMetadata { + //XCHEDDAR icon + let data_url = " + + + + + + + "; + + FungibleTokenMetadata { + spec: FT_METADATA_SPEC.to_string(), + name: String::from("xCheddar Token"), + symbol: String::from("xCHEDDAR"), + icon: Some(String::from(data_url)), + reference: None, + reference_hash: None, + decimals: 24, + } + } +} diff --git a/xcheddar/src/owner.rs b/xcheddar/src/owner.rs new file mode 100644 index 0000000..a195bfe --- /dev/null +++ b/xcheddar/src/owner.rs @@ -0,0 +1,106 @@ +//! Implement all the relevant logic for owner of this contract. + +use crate::*; + +#[near_bindgen] +impl Contract { + pub fn set_owner(&mut self, owner_id: AccountId) { + self.assert_owner(); + assert!( + env::is_valid_account_id(owner_id.as_bytes()), + "Account @{} is invalid!", + owner_id + ); + self.owner_id = owner_id; + } + + pub fn get_owner(&self) -> AccountId { + self.owner_id.clone() + } + + pub fn set_reward_per_second(&mut self, reward_per_second: U128, distribute_before_change: bool) { + self.assert_owner(); + if distribute_before_change { + self.distribute_reward(); + } + self.reward_per_second = reward_per_second.into(); + } + + pub fn reset_reward_genesis_time_in_sec(&mut self, reward_genesis_time_in_sec: u32) { + self.assert_owner(); + let cur_time = nano_to_sec(env::block_timestamp()); + if reward_genesis_time_in_sec < cur_time { + panic!("{}", ERR_RESET_TIME_IS_PAST_TIME); + } else if self.reward_genesis_time_in_sec < cur_time { + panic!("{}", ERR_REWARD_GENESIS_TIME_PASSED); + } + self.reward_genesis_time_in_sec = reward_genesis_time_in_sec; + self.prev_distribution_time_in_sec = reward_genesis_time_in_sec; + } + + pub(crate) fn assert_owner(&self) { + assert_eq!( + env::predecessor_account_id(), + self.owner_id, + "{}", ERR_NOT_ALLOWED + ); + } + + // State 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 super::*; + use near_sdk::env; + use near_sdk::Gas; + use near_sys as sys; + /// 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. + /// After upgrade we call *pub fn migrate()* on the NEW CONTRACT CODE + #[no_mangle] + pub fn upgrade() { + /// Gas for calling migration call. One Tera - 1 TGas + pub const GAS_FOR_MIGRATE_CALL: Gas = Gas(5_000_000_000_000); + /// 20 Tgas + pub const GAS_FOR_UPGRADE: Gas = Gas(20_000_000_000_000); + const BLOCKCHAIN_INTERFACE_NOT_SET_ERR: &str = "Blockchain interface not set."; + + env::setup_panic_hook(); + + /// assert ownership + let contract: Contract = env::state_read().expect("ERR_CONTRACT_IS_NOT_INITIALIZED"); + contract.assert_owner(); + + let current_id = env::current_account_id(); + let migrate_method_name = "migrate".as_bytes().to_vec(); + let attached_gas = env::prepaid_gas() - env::used_gas() - GAS_FOR_UPGRADE; + unsafe { + // Load input (NEW CONTRACT CODE) into register 0. + sys::input(0); + // prepare self-call promise + let promise_id = sys::promise_batch_create(current_id.as_bytes().len() as _, current_id.as_bytes().as_ptr() as _); + + // #Action_1 - deploy/upgrade code from register 0 + sys::promise_batch_action_deploy_contract(promise_id, u64::MAX as _, 0); + // #Action_2 - schedule a call for migrate + // Execute on NEW CONTRACT CODE + sys::promise_batch_action_function_call( + promise_id, + migrate_method_name.len() as _, + migrate_method_name.as_ptr() as _, + 0 as _, + 0 as _, + 0 as _, + u64::from(attached_gas), + ); + } + } +} \ No newline at end of file diff --git a/xcheddar/src/storage_impl.rs b/xcheddar/src/storage_impl.rs new file mode 100644 index 0000000..4eab999 --- /dev/null +++ b/xcheddar/src/storage_impl.rs @@ -0,0 +1,49 @@ +use crate::*; +use near_contract_standards::storage_management::{ + StorageBalance, StorageBalanceBounds, StorageManagement, +}; + +use near_sdk::json_types::U128; +use near_sdk::near_bindgen; + +#[near_bindgen] +impl StorageManagement for Contract { + #[payable] + fn storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + let local_account_id = + account_id.clone().map(|a| a.into()).unwrap_or_else(|| env::predecessor_account_id()); + if !self.ft.accounts.contains_key(&local_account_id) { + self.account_number += 1; + } + self.ft.storage_deposit(account_id, registration_only) + } + + #[payable] + fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + self.ft.storage_withdraw(amount) + } + + #[payable] + fn storage_unregister(&mut self, force: Option) -> bool { + #[allow(unused_variables)] + if let Some((account_id, balance)) = self.ft.internal_storage_unregister(force) { + let number = self.account_number.checked_sub(1).unwrap_or(0); + self.account_number = number; + true + } else { + false + } + } + + fn storage_balance_bounds(&self) -> StorageBalanceBounds { + self.ft.storage_balance_bounds() + } + + fn storage_balance_of(&self, account_id: AccountId) -> Option { + self.ft.storage_balance_of(account_id) + } +} \ No newline at end of file diff --git a/xcheddar/src/utils.rs b/xcheddar/src/utils.rs new file mode 100644 index 0000000..80ec6ef --- /dev/null +++ b/xcheddar/src/utils.rs @@ -0,0 +1,41 @@ +use chrono::prelude::*; +use near_sdk::{Balance, Gas, Timestamp}; + +use uint::construct_uint; + +pub const CHEDDAR_DECIMALS: u8 = 24; +pub const ONE_YOCTO: u128 = 1; +pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(10_000_000_000_000); +pub const GAS_FOR_FT_TRANSFER: Gas = Gas(20_000_000_000_000); + +pub const DURATION_30DAYS_IN_SEC: u32 = 60 * 60 * 24 * 30; + +pub const ERR_RESET_TIME_IS_PAST_TIME: &str = "Used reward_genesis_time_in_sec must be less than current time!"; +pub const ERR_REWARD_GENESIS_TIME_PASSED: &str = "Setting in contract Genesis time must be less than current time!"; +pub const ERR_NOT_ALLOWED: &str = "Owner's method"; +pub const ERR_NOT_INITIALIZED: &str = "State was not initialized!"; +pub const ERR_INTERNAL: &str = "Amount of locked token must be greater than 0"; +pub const ERR_STAKE_TOO_SMALL: &str = "Stake more than 0 tokens"; +pub const ERR_EMPTY_TOTAL_SUPPLY: &str = "Total supply cannot be empty!"; +pub const ERR_KEEP_AT_LEAST_ONE_XCHEDDAR: &str = "At least 1 Cheddar must be on lockup contract account"; +pub const ERR_MISMATCH_TOKEN: &str = "Only Cheddar tokrn contract may calls this lockup contract"; +pub const ERR_PROMISE_RESULT: &str = "Expected 1 promise result"; +pub const ERR_WRONG_TRANSFER_MSG: &str = "Use empty msg for deposit to stake Cheddar or msg = 'reward' for add some reward to contract"; + +construct_uint! { + // 256-bit unsigned integer. + pub struct U256(4); +} + +pub fn nano_to_sec(nano: Timestamp) -> u32 { + (nano / 1_000_000_000) as u32 +} + +pub fn convert_from_yocto_cheddar(yocto_amount: Balance) -> u128 { + (yocto_amount + (5 * 10u128.pow((CHEDDAR_DECIMALS - 1u8).into()))) / 10u128.pow(CHEDDAR_DECIMALS.into()) +} +pub fn convert_timestamp_to_datetime(timestamp: u32) -> DateTime { + let naive_datetime = NaiveDateTime::from_timestamp(timestamp.into(), 0); + DateTime::from_utc(naive_datetime, Utc) +} + diff --git a/xcheddar/src/views.rs b/xcheddar/src/views.rs new file mode 100644 index 0000000..4e83e0c --- /dev/null +++ b/xcheddar/src/views.rs @@ -0,0 +1,108 @@ +//! View functions for the contract. +use crate::{*, utils::convert_from_yocto_cheddar, utils::convert_timestamp_to_datetime}; +use chrono::{DateTime, Utc}; +use near_sdk::serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Deserialize, Debug))] +pub struct ContractMetadata { + pub version: String, + pub owner_id: AccountId, + pub locked_token: AccountId, + // at prev_distribution_time, the amount of undistributed reward + pub undistributed_reward: U128, + // at prev_distribution_time, the amount of staked token + pub locked_token_amount: U128, + // at call time, the amount of undistributed reward + pub cur_undistributed_reward: U128, + // at call time, the amount of staked token + pub cur_locked_token_amount: U128, + // cur XCHEDDAR supply + pub supply: U128, + pub prev_distribution_time_in_sec: u32, + pub reward_genesis_time_in_sec: u32, + pub reward_per_second: U128, + /// current account number in contract + pub account_number: u64, +} +#[derive(Serialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Deserialize, Debug))] +pub struct ContractMetadataHumanReadable { + pub version: String, + pub owner_id: AccountId, + pub locked_token: AccountId, + // at prev_distribution_time, the amount of undistributed reward + pub undistributed_reward: U128, + // at prev_distribution_time, the amount of staked token + pub locked_token_amount: U128, + // at call time, the amount of undistributed reward + pub cur_undistributed_reward: U128, + // at call time, the amount of staked token + pub cur_locked_token_amount: U128, + // cur XCHEDDAR supply + pub supply: U128, + pub prev_distribution_time: DateTime, + pub reward_genesis_time: DateTime, + pub reward_per_second: U128, + /// current account number in contract + pub account_number: u64, +} + +#[near_bindgen] +impl Contract { + /// Return contract basic info + pub fn contract_metadata(&self) -> ContractMetadata { + //check + let to_be_distributed = + self.try_distribute_reward(nano_to_sec(env::block_timestamp())); + ContractMetadata { + version: env!("CARGO_PKG_VERSION").to_string(), + owner_id: self.owner_id.clone(), + locked_token: self.locked_token.clone(), + undistributed_reward: self.undistributed_reward.into(), + locked_token_amount: self.locked_token_amount.into(), + cur_undistributed_reward: (self.undistributed_reward - to_be_distributed).into(), + cur_locked_token_amount: (self.locked_token_amount + to_be_distributed).into(), + supply: self.ft.total_supply.into(), + prev_distribution_time_in_sec: self.prev_distribution_time_in_sec, + reward_genesis_time_in_sec: self.reward_genesis_time_in_sec, + reward_per_second: self.reward_per_second.into(), + account_number: self.account_number, + } + } + /// Return contract basic info with human-readable balances + pub fn contract_metadata_human_readable(&self) -> ContractMetadataHumanReadable { + //check + let to_be_distributed = + self.try_distribute_reward(nano_to_sec(env::block_timestamp())); + ContractMetadataHumanReadable { + version: env!("CARGO_PKG_VERSION").to_string(), + owner_id: self.owner_id.clone(), + locked_token: self.locked_token.clone(), + undistributed_reward: convert_from_yocto_cheddar(self.undistributed_reward).into(), + locked_token_amount: convert_from_yocto_cheddar(self.locked_token_amount).into(), + cur_undistributed_reward: convert_from_yocto_cheddar(self.undistributed_reward - to_be_distributed).into(), + cur_locked_token_amount: convert_from_yocto_cheddar(self.locked_token_amount + to_be_distributed).into(), + supply: convert_from_yocto_cheddar(self.ft.total_supply).into(), + prev_distribution_time: convert_timestamp_to_datetime(self.prev_distribution_time_in_sec), + reward_genesis_time: convert_timestamp_to_datetime(self.reward_genesis_time_in_sec), + reward_per_second: convert_from_yocto_cheddar(self.reward_per_second).into(), + account_number: self.account_number, + } + } + + // get the X-Cheddar / Cheddar price in decimal 8 + pub fn get_virtual_price(&self) -> U128 { + if self.ft.total_supply == 0 { + 100_000_000.into() + } else { + ((self.locked_token_amount + + self.try_distribute_reward(nano_to_sec(env::block_timestamp()))) + * 100_000_000 + / self.ft.total_supply) + .into() + } + } +} diff --git a/xcheddar/src/xcheddar.rs b/xcheddar/src/xcheddar.rs new file mode 100644 index 0000000..8440255 --- /dev/null +++ b/xcheddar/src/xcheddar.rs @@ -0,0 +1,314 @@ +use crate::*; +#[allow(unused_imports)] +use crate::utils::convert_from_yocto_cheddar; +use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +use near_sdk::json_types::U128; +use near_sdk::{assert_one_yocto, env, log, Promise, PromiseResult}; +use std::cmp::{max, min}; + +enum TransferInstruction { + Deposit, + Reward, + Unknown +} + +impl From for TransferInstruction { + fn from(msg: String) -> Self { + match &msg[..] { + "" => TransferInstruction::Deposit, + "reward" => TransferInstruction::Reward, + _ => TransferInstruction::Unknown + } + } +} + +impl Contract { + pub(crate) fn internal_stake(&mut self, account_id: &AccountId, amount: Balance) { + // check account has registered + assert!(self.ft.accounts.contains_key(account_id), "Account @{} is not registered", account_id); + // amount of Xcheddar that user takes from stake cheddar + let mut minted = amount; + + if self.ft.total_supply != 0 { + assert!(self.locked_token_amount > 0, "{}", ERR_INTERNAL); + minted = (U256::from(amount) * U256::from(self.ft.total_supply) / U256::from(self.locked_token_amount)).as_u128(); + } + + assert!(minted > 0, "{}", ERR_STAKE_TOO_SMALL); + + // increase locked_token_amount to staked + self.locked_token_amount += amount; + // increase total_supply to minted = staked * P, where P = total_supply/locked_token_amount <= 1 + self.ft.internal_deposit(account_id, minted); + log!("@{} Stake {} (~{} CHEDDAR) assets, get {} (~{} xCHEDDAR) tokens", + account_id, + amount, + convert_from_yocto_cheddar(amount), + minted, + convert_from_yocto_cheddar(minted) + ); + // total_supply += amount * P, where P<=1 + // locked_token_amount += amount + } + + pub(crate) fn internal_add_reward(&mut self, account_id: &AccountId, amount: Balance) { + self.undistributed_reward += amount; + log!("@{} add {} (~{} CHEDDAR) assets as reward", account_id, amount, convert_from_yocto_cheddar(amount)); + } + + // return the amount of to be distribute reward this time + pub(crate) fn try_distribute_reward(&self, cur_timestamp_in_sec: u32) -> Balance { + if cur_timestamp_in_sec > self.reward_genesis_time_in_sec && cur_timestamp_in_sec > self.prev_distribution_time_in_sec { + //reward * (duration between previous distribution and current time) + let ideal_amount = self.reward_per_second * (cur_timestamp_in_sec - self.prev_distribution_time_in_sec) as u128; + min(ideal_amount, self.undistributed_reward) + } else { + 0 + } + } + + pub(crate) fn distribute_reward(&mut self) { + let cur_time = nano_to_sec(env::block_timestamp()); + let new_reward = self.try_distribute_reward(cur_time); + if new_reward > 0 { + self.undistributed_reward -= new_reward; + self.locked_token_amount += new_reward; + self.prev_distribution_time_in_sec = max(cur_time, self.reward_genesis_time_in_sec); + log!("Distribution reward is {} ", new_reward); + } else { + log!("Distribution reward is zero for this time"); + } + } +} + +#[near_bindgen] +impl Contract { + /// unstake token and send assets back to the predecessor account. + /// Requirements: + /// * The predecessor account should be registered. + /// * `amount` must be a positive integer. + /// * The predecessor account should have at least the `amount` of tokens. + /// * Requires attached deposit of exactly 1 yoctoNEAR. + /// ? : withdraw on every time or it opens in windows? + #[payable] + pub fn unstake(&mut self, amount: U128) -> Promise { + // Checkpoint + self.distribute_reward(); + + assert_one_yocto(); + + let account_id = env::predecessor_account_id(); + let amount: Balance = amount.into(); + + assert!(self.ft.total_supply > 0, "{}", ERR_EMPTY_TOTAL_SUPPLY); + let unlocked = (U256::from(amount) * U256::from(self.locked_token_amount) / U256::from(self.ft.total_supply)).as_u128(); + + // total_supply -= amount + self.ft.internal_withdraw(&account_id, amount); + assert!(self.ft.total_supply >= 10u128.pow(24), "{}", ERR_KEEP_AT_LEAST_ONE_XCHEDDAR); + // locked_token_amount -= amount * P, where P = locked_token_amount / total_supply >=1 + self.locked_token_amount -= unlocked; + + log!("Withdraw {} (~{} Cheddar) from @{}", amount, convert_from_yocto_cheddar(amount), account_id); + + // ext_fungible_token was deprecated at v4.0.0 near_sdk release + /* + ext_fungible_token::ft_transfer( + account_id.clone(), + U128(unlocked), + None, + self.locked_token.clone(), + 1, + GAS_FOR_FT_TRANSFER, + ) + .then(ext_self::callback_post_unstake( + account_id.clone(), + U128(unlocked), + U128(amount), + env::current_account_id(), + NO_DEPOSIT, + GAS_FOR_RESOLVE_TRANSFER, + )) + */ + + ext_cheddar::ext(self.locked_token.clone()) + .with_attached_deposit(ONE_YOCTO) + .with_static_gas(GAS_FOR_FT_TRANSFER) + .ft_transfer(account_id.clone(), U128(unlocked), None) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) + .callback_post_unstake(account_id.clone(), U128(unlocked), U128(amount)) + ) + } + + #[private] + pub fn callback_post_unstake( + &mut self, + sender_id: AccountId, + amount: U128, + share: U128, + ) { + assert_eq!( + env::promise_results_count(), + 1, + "{}", ERR_PROMISE_RESULT + ); + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => { + log!( + "Account @{} successful unstake {} (~{} CHEDDAR).", + sender_id, + amount.0, + convert_from_yocto_cheddar(amount.0) + ); + } + PromiseResult::Failed => { + // This reverts the changes from unstake function. + // If account doesn't exit, the unlock token stay in contract. + if self.ft.accounts.contains_key(&sender_id) { + self.locked_token_amount += amount.0; + self.ft.internal_deposit(&sender_id, share.0); + log!( + "Account @{} unstake failed and reverted.", + sender_id + ); + } else { + log!( + "Account @{} has unregistered. Unlocking token goes to contract.", + sender_id + ); + } + } + }; + } +} + +#[near_bindgen] +impl FungibleTokenReceiver for Contract { + fn ft_on_transfer( + &mut self, + sender_id: AccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue { + // Checkpoint + self.distribute_reward(); + let token_in = env::predecessor_account_id(); + let amount: Balance = amount.into(); + assert_eq!(token_in, self.locked_token, "{}", ERR_MISMATCH_TOKEN); + match TransferInstruction::from(msg) { + TransferInstruction::Deposit => { + // deposit for stake + self.internal_stake(&sender_id, amount); + PromiseOrValue::Value(U128(0)) + } + TransferInstruction::Reward => { + // deposit for reward + self.internal_add_reward(&sender_id, amount); + PromiseOrValue::Value(U128(0)) + } + TransferInstruction::Unknown => { + log!(ERR_WRONG_TRANSFER_MSG); + PromiseOrValue::Value(U128(amount)) + } + } + } +} +#[cfg(test)] +mod tests { + use super::*; + const E24:u128 = 1_000_000_000_000_000_000_000_000; + + fn proportion(a:u128, numerator:u128, denominator:u128) -> u128 { + (U256::from(a) * U256::from(numerator) / U256::from(denominator)).as_u128() + } + fn compute_p(total_locked: u128, total_supply:u128, staked:bool) -> u128 { + if staked == true { + total_supply * 100_000_000 / total_locked + } else { + total_locked * 100_000_000 / total_supply + } + } + #[test] + fn test_P_value() { + + let mut total_reward:u128 = 50_000 * E24; + let mut total_locked:u128 = 52_500 * E24; + let mut total_supply:u128 = 50_000 * E24; + let reward_per_second:u128 = 10000000000000000000000; //0.01 + + let p_unstaked = compute_p(total_locked, total_supply, false); // 1.05 + let p_staked = compute_p(total_locked, total_supply, true); // 1/1.05 + + // stake 100 + let amount:u128 = 100 * E24; //100 + let minted = proportion(amount, total_supply, total_locked); + total_locked += amount; + total_supply += minted; + assert_eq!(p_staked, compute_p(total_locked, total_supply, true)); + assert_eq!(p_unstaked, compute_p(total_locked, total_supply, false)); + println!( + " P_staked: {}\n P_unstaked: {}\n P-deviation: {}\n locked: {}\n supply: {}", + compute_p(total_locked, total_supply, true), + compute_p(total_locked, total_supply, false), + convert_from_yocto_cheddar((p_staked - compute_p(total_locked, total_supply, true))), + total_locked, + total_supply + ); + + // stake 1000 + let amount:u128 = 1000 * E24; //1000 + let minted = proportion(amount, total_supply, total_locked); + total_locked += amount; + total_supply += minted; + assert_eq!(p_staked, compute_p(total_locked, total_supply, true)); + assert_eq!(p_unstaked, compute_p(total_locked, total_supply, false)); + println!( + " P_staked: {}\n P_unstaked: {}\n P-deviation: {}\n locked: {}\n supply: {}", + compute_p(total_locked, total_supply, true), + compute_p(total_locked, total_supply, false), + convert_from_yocto_cheddar((p_staked - compute_p(total_locked, total_supply, true))), + total_locked, + total_supply + ); + + // unstake 10000 after 1000 seconds + // distribution + total_locked += reward_per_second * 1000; + let amount:u128 = 10000 * E24; //10000 + // unstaking + let unlocked = proportion(amount, total_locked, total_supply); + total_locked -= unlocked; + total_supply -= amount; + println!( + " P_staked: {}\n P_unstaked: {}\n P-deviation: {}\n locked: {}\n supply: {}", + compute_p(total_locked, total_supply, true), + compute_p(total_locked, total_supply, false), + convert_from_yocto_cheddar((p_staked - compute_p(total_locked, total_supply, true))), + total_locked, + total_supply + ); + + // unstake all and keep 1 token in supply after 1000 seconds + // distribution + total_locked += reward_per_second * 1000; + let amount:u128 = 41046619047619047619047619047; + // unstaking + let unlocked = proportion(amount, total_locked, total_supply); + total_locked -= unlocked; + total_supply -= amount; + println!( + " P_staked: {}\n P_unstaked: {}\n P-deviation: {}\n locked: {}\n supply: {}", + compute_p(total_locked, total_supply, true), + compute_p(total_locked, total_supply, false), + convert_from_yocto_cheddar((p_staked - compute_p(total_locked, total_supply, true))), + total_locked, + total_supply + ); + + + + } +} \ No newline at end of file diff --git a/xcheddar/tests/common/init.rs b/xcheddar/tests/common/init.rs new file mode 100644 index 0000000..5302592 --- /dev/null +++ b/xcheddar/tests/common/init.rs @@ -0,0 +1,61 @@ +use near_sdk_sim::{call, deploy, init_simulator, to_yocto, ContractAccount, UserAccount}; + +use cheddar_coin::ContractContract as CheddarToken; +use xcheddar_token::ContractContract as XCheddarToken; + +near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { + TEST_WASM_BYTES => "./res/cheddar_coin.wasm", + XCHEDDAR_WASM_BYTES => "./res/xcheddar_token.wasm", +} + +pub fn init_env(register_user: bool) -> (UserAccount, UserAccount, UserAccount, ContractAccount, ContractAccount){ + let root = init_simulator(None); + + let owner = root.create_user("owner".parse().unwrap(), to_yocto("100")); + let user = root.create_user("user".parse().unwrap(), to_yocto("100")); + + let cheddar_contract = deploy!( + contract: CheddarToken, + contract_id: "cheddar", + bytes: &TEST_WASM_BYTES, + signer_account: root + ); + call!(root, cheddar_contract.new(owner.account_id())).assert_success(); + call!(owner, cheddar_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + call!(user, cheddar_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + + call!( + owner, + cheddar_contract.add_minter(owner.account_id()), + deposit = 1 + ); + call!( + owner, + cheddar_contract.add_minter(user.account_id()), + deposit = 1 + ); + + call!( + owner, + cheddar_contract.ft_mint(&owner.account_id(), to_yocto("10000").into(), None), + deposit = 1 + ).assert_success(); + call!( + user, + cheddar_contract.ft_mint(&user.account_id(), to_yocto("100").into(), None), + deposit = 1 + ).assert_success(); + + let xcheddar_contract = deploy!( + contract: XCheddarToken, + contract_id: "xcheddar", + bytes: &XCHEDDAR_WASM_BYTES, + signer_account: root + ); + call!(root, xcheddar_contract.new(owner.account_id(), cheddar_contract.account_id())).assert_success(); + call!(root, cheddar_contract.storage_deposit(Some(xcheddar_contract.account_id()), None), deposit = to_yocto("1")).assert_success(); + if register_user { + call!(user, xcheddar_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + } + (root, owner, user, cheddar_contract, xcheddar_contract) +} \ No newline at end of file diff --git a/xcheddar/tests/common/mod.rs b/xcheddar/tests/common/mod.rs new file mode 100644 index 0000000..d050952 --- /dev/null +++ b/xcheddar/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod init; +pub mod utils; \ No newline at end of file diff --git a/xcheddar/tests/common/utils.rs b/xcheddar/tests/common/utils.rs new file mode 100644 index 0000000..11b6f2a --- /dev/null +++ b/xcheddar/tests/common/utils.rs @@ -0,0 +1,39 @@ +#![allow(unused)] +use xcheddar_token::ContractMetadata; +use near_sdk_sim::ExecutionResult; +use uint::construct_uint; + +pub const DURATION_30DAYS_IN_SEC: u32 = 60 * 60 * 24 * 30; + +construct_uint! { + /// 256-bit unsigned integer. + pub struct U256(4); +} + +pub fn assert_xcheddar( + current_xcheddar: &ContractMetadata, + undistributed_reward: u128, + locked_token_amount: u128, + supply: u128, + +) { + assert_eq!(current_xcheddar.undistributed_reward.0, undistributed_reward); + assert_eq!(current_xcheddar.locked_token_amount.0, locked_token_amount); + assert_eq!(current_xcheddar.supply.0, supply); +} + +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 as u64 +} diff --git a/xcheddar/tests/test_migrate.rs b/xcheddar/tests/test_migrate.rs new file mode 100644 index 0000000..1f4ee78 --- /dev/null +++ b/xcheddar/tests/test_migrate.rs @@ -0,0 +1,55 @@ + +use near_sdk_sim::{deploy, view, init_simulator, to_yocto}; + +use xcheddar_token::{ContractContract as XCheddar, ContractMetadata}; + +near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { + PREV_XCHEDDAR_WASM_BYTES => "./res/xcheddar_token.wasm", + XCHEDDAR_WASM_BYTES => "./res/xcheddar_token.wasm", +} + +#[test] +fn test_upgrade() { + let root = init_simulator(None); + let test_user = root.create_user("test".parse().unwrap(), to_yocto("100")); + let xcheddar = deploy!( + contract: XCheddar, + contract_id: "xcheddar".to_string(), + bytes: &PREV_XCHEDDAR_WASM_BYTES, + signer_account: root, + init_method: new(root.account_id(), root.account_id()) + ); + // Failed upgrade with no permissions. + let result = test_user + .call( + xcheddar.account_id().clone(), + "upgrade", + &XCHEDDAR_WASM_BYTES, + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .status(); + assert!(format!("{:?}", result).contains("Owner's method")); + + root.call( + xcheddar.account_id().clone(), + "upgrade", + &XCHEDDAR_WASM_BYTES, + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); + let metadata = view!(xcheddar.contract_metadata()).unwrap_json::(); + // println!("{:#?}", metadata); + assert_eq!(metadata.version, "1.0.2".to_string()); + + // Upgrade to the same code migration is skipped. + root.call( + xcheddar.account_id().clone(), + "upgrade", + &XCHEDDAR_WASM_BYTES, + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); +} \ No newline at end of file diff --git a/xcheddar/tests/test_owner.rs b/xcheddar/tests/test_owner.rs new file mode 100644 index 0000000..00a491c --- /dev/null +++ b/xcheddar/tests/test_owner.rs @@ -0,0 +1,154 @@ +use near_sdk_sim::{call, view, to_yocto}; +use xcheddar_token::ContractMetadata; +use near_sdk::json_types::U128; + +mod common; +use crate::common::{ + init::*, + utils::* +}; + +pub const DURATION_30DAYS_IN_SEC: u32 = 60 * 60 * 24 * 30; +pub const DURATION_1DAY_IN_SEC: u32 = 60 * 60 * 24; + +#[test] +//passed +fn test_reset_reward_genesis_time(){ + let (root, owner, _, cheddar_contract, xcheddar_contract) = + init_env(true); + + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let init_genesis_time = xcheddar_info.reward_genesis_time_in_sec; + assert_eq!(init_genesis_time, xcheddar_info.prev_distribution_time_in_sec); + + // reward_distribute won't touch anything before genesis time + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), true) + ) + .assert_success(); + call!( + owner, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("100").into(), None, "reward".to_string()), + deposit = 1 + ) + .assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(init_genesis_time, xcheddar_info.reward_genesis_time_in_sec); + assert_eq!(init_genesis_time, xcheddar_info.prev_distribution_time_in_sec); + assert_eq!(U128(to_yocto("100")), xcheddar_info.undistributed_reward); + assert_eq!(U128(to_yocto("1")), xcheddar_info.monthly_reward); + assert_eq!(U128(to_yocto("100")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("0")), xcheddar_info.cur_locked_token_amount); + + // and reward won't be distributed before genesis time + root.borrow_runtime_mut().cur_block.block_timestamp = 100_000_000_000; + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(U128(to_yocto("100")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("0")), xcheddar_info.cur_locked_token_amount); + + // and nothing happen even if some action invoke the reward distribution before genesis time + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("5").into(), true) + ) + .assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(init_genesis_time, xcheddar_info.reward_genesis_time_in_sec); + assert_eq!(init_genesis_time, xcheddar_info.prev_distribution_time_in_sec); + assert_eq!(U128(to_yocto("100")), xcheddar_info.undistributed_reward); + assert_eq!(U128(to_yocto("5")), xcheddar_info.monthly_reward); + assert_eq!(U128(to_yocto("100")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("0")), xcheddar_info.cur_locked_token_amount); + + // change genesis time would also change prev_distribution_time_in_sec + let current_timestamp = root.borrow_runtime().cur_block.block_timestamp; + call!( + owner, + xcheddar_contract.reset_reward_genesis_time_in_sec(nano_to_sec(current_timestamp) + 50) + ).assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.reward_genesis_time_in_sec, nano_to_sec(current_timestamp) + 50); + assert_eq!(xcheddar_info.prev_distribution_time_in_sec, nano_to_sec(current_timestamp) + 50); + assert_eq!(U128(to_yocto("100")), xcheddar_info.undistributed_reward); + assert_eq!(U128(to_yocto("5")), xcheddar_info.monthly_reward); + assert_eq!(U128(to_yocto("100")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("0")), xcheddar_info.cur_locked_token_amount); + + // 2 month passed + root.borrow_runtime_mut().cur_block.block_timestamp = current_timestamp + sec_to_nano(2 * DURATION_30DAYS_IN_SEC); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(U128(to_yocto("95")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("5")), xcheddar_info.cur_locked_token_amount); + // when some call invoke reward distribution after reward genesis time + root.borrow_runtime_mut().cur_block.block_timestamp = current_timestamp + sec_to_nano(DURATION_30DAYS_IN_SEC + DURATION_1DAY_IN_SEC); + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("10").into(), true) + ) + .assert_success(); + //3 month passed + root.borrow_runtime_mut().cur_block.block_timestamp = current_timestamp + sec_to_nano(3 * DURATION_30DAYS_IN_SEC); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.reward_genesis_time_in_sec, nano_to_sec(current_timestamp) + 50); + assert_eq!(xcheddar_info.prev_distribution_time_in_sec, nano_to_sec(current_timestamp) + 2678401); + assert_eq!(U128(to_yocto("95")), xcheddar_info.undistributed_reward); + assert_eq!(U128(to_yocto("5")), xcheddar_info.locked_token_amount); + assert_eq!(U128(to_yocto("10")), xcheddar_info.monthly_reward); + assert_eq!(U128(to_yocto("85")), xcheddar_info.cur_undistributed_reward); + assert_eq!(U128(to_yocto("15")), xcheddar_info.cur_locked_token_amount); + +} +//passed +#[test] +fn test_reset_reward_genesis_time_use_past_time(){ + let (root, owner, _, _, xcheddar_contract) = + init_env(true); + + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + + let current_timestamp = root.borrow_runtime().cur_block.block_timestamp; + let out_come = call!( + owner, + xcheddar_contract.reset_reward_genesis_time_in_sec(nano_to_sec(current_timestamp) - 1) + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("Used reward_genesis_time_in_sec must be less than current time!")); + + let xcheddar_info1 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.reward_genesis_time_in_sec, xcheddar_info1.reward_genesis_time_in_sec); +} +//passed +#[test] +fn test_reward_genesis_time_passed(){ + let (root, owner, _, _, xcheddar_contract) = + init_env(true); + + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + + root.borrow_runtime_mut().cur_block.block_timestamp = (xcheddar_info.reward_genesis_time_in_sec + 1) as u64 * 1_000_000_000; + let current_timestamp = root.borrow_runtime().cur_block.block_timestamp; + let out_come = call!( + owner, + xcheddar_contract.reset_reward_genesis_time_in_sec(nano_to_sec(current_timestamp) + 1) + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("Setting in contract Genesis time must be less than current time!")); + + let xcheddar_info1 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.reward_genesis_time_in_sec, xcheddar_info1.reward_genesis_time_in_sec); +} + +#[test] +fn test_modify_monthly_reward(){ + let (_, owner, _, _, xcheddar_contract) = + init_env(true); + + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), true) + ) + .assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.monthly_reward.0, to_yocto("1")); +} \ No newline at end of file diff --git a/xcheddar/tests/test_reward.rs b/xcheddar/tests/test_reward.rs new file mode 100644 index 0000000..1435612 --- /dev/null +++ b/xcheddar/tests/test_reward.rs @@ -0,0 +1,282 @@ +use near_sdk_sim::{call, view, to_yocto}; +use xcheddar_token::ContractMetadata; +use near_sdk::json_types::U128; + +mod common; +use crate::common::{ + init::*, + utils::* +}; +//failed +#[test] +fn test_reward() { + let (root, owner, user, cheddar_contract, xcheddar_contract) = + init_env(true); + let mut total_reward = 0; + let mut total_locked = 0; + let mut total_supply = 0; + + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), true) + ) + .assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.monthly_reward.0, to_yocto("1")); + + let current_timestamp = root.borrow_runtime_mut().cur_block.block_timestamp; + call!( + owner, + xcheddar_contract.reset_reward_genesis_time_in_sec(nano_to_sec(current_timestamp) + 10) + ).assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.reward_genesis_time_in_sec, nano_to_sec(current_timestamp) + 10); + + root.borrow_runtime_mut().cur_block.block_timestamp = sec_to_nano(nano_to_sec(current_timestamp) + 10); + + //add reward trigger distribute_reward, just update prev_distribution_time + call!( + owner, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("100").into(), None, "reward".to_string()), + deposit = 1 + ) + .assert_success(); + total_reward += to_yocto("100"); + + let xcheddar_info0 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(&xcheddar_info0, to_yocto("100"), 0, 0); + assert_eq!(to_yocto("1"), xcheddar_info0.monthly_reward.0); + + + //stake trigger distribute_reward + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("11").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + total_locked += to_yocto("11"); + total_supply += to_yocto("11"); + + let xcheddar_info1 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = (xcheddar_info1.prev_distribution_time_in_sec - xcheddar_info0.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + total_reward -= time_diff as u128 * xcheddar_info1.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info1.monthly_reward.0; + assert_xcheddar(&xcheddar_info1, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("89"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + assert!(root.borrow_runtime_mut().produce_block().is_ok()); + + //modify_monthly_reward trigger distribute_reward + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), true) + ) + .assert_success(); + let xcheddar_info2 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info2.monthly_reward.0, to_yocto("1")); + + let time_diff = (xcheddar_info2.prev_distribution_time_in_sec - xcheddar_info1.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + total_reward -= time_diff as u128 * xcheddar_info2.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info2.monthly_reward.0; + assert_xcheddar(&xcheddar_info2, total_reward, total_locked, total_supply); + + assert!(root.borrow_runtime_mut().produce_block().is_ok()); + + //modify_monthly_reward not trigger distribute_reward + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), false) + ) + .assert_success(); + let xcheddar_info2_1 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + + let time_diff = (xcheddar_info2_1.prev_distribution_time_in_sec - xcheddar_info2.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + total_reward -= time_diff as u128 * xcheddar_info2_1.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info2_1.monthly_reward.0; + assert_xcheddar(&xcheddar_info2_1, total_reward, total_locked, total_supply); + assert_eq!(time_diff, 0); + + assert!(root.borrow_runtime_mut().produce_block().is_ok()); + + //nothing trigger distribute_reward + let xcheddar_info3 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(&xcheddar_info3, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("89"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + //add reward trigger distribute_reward + call!( + owner, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("100").into(), None, "reward".to_string()), + deposit = 1 + ) + .assert_success(); + total_reward += to_yocto("100"); + + let xcheddar_info4 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = (xcheddar_info4.prev_distribution_time_in_sec - xcheddar_info3.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + total_reward -= time_diff as u128 * xcheddar_info4.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info4.monthly_reward.0; + assert_xcheddar(&xcheddar_info4, total_reward, total_locked, total_supply); + + assert!(root.borrow_runtime_mut().produce_block().is_ok()); + + //unstake trigger distribute_reward + call!( + user, + xcheddar_contract.unstake(to_yocto("10").into()), + deposit = 1 + ) + .assert_success(); + + let xcheddar_info5 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = (xcheddar_info5.prev_distribution_time_in_sec - xcheddar_info4.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + total_reward -= time_diff as u128 * xcheddar_info5.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info5.monthly_reward.0; + + let unlocked = (U256::from(to_yocto("10")) * U256::from(total_locked) / U256::from(total_supply)).as_u128(); + total_locked -= unlocked; + total_supply -= to_yocto("10"); + + assert_eq!(to_yocto("1"), total_supply); + assert_xcheddar(&xcheddar_info5, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("89") + unlocked, view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + assert!(root.borrow_runtime_mut().produce_blocks(1000).is_ok()); + + //nothing trigger distribute_reward + let xcheddar_info6 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(&xcheddar_info6, total_reward, total_locked, total_supply); + + //stake trigger distribute_reward,total_reward less then distribute_reward + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + let xcheddar_info7 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = (xcheddar_info7.prev_distribution_time_in_sec - xcheddar_info6.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC; + assert!(total_reward < time_diff as u128 * xcheddar_info7.monthly_reward.0); + total_locked += total_reward; + total_reward -= total_reward; + + total_supply += (U256::from(to_yocto("10")) * U256::from(total_supply) / U256::from(total_locked)).as_u128(); + total_locked += to_yocto("10"); + + assert_xcheddar(&xcheddar_info7, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("79") + unlocked, view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + //stake when total_locked contains reward + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + total_supply += (U256::from(to_yocto("10")) * U256::from(total_supply) / U256::from(total_locked)).as_u128(); + total_locked += to_yocto("10"); + + let xcheddar_info8 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(&xcheddar_info8, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("69") + unlocked, view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); +} + +#[test] +fn test_no_reward_before_reset_reward_genesis_time(){ + let (root, owner, user, cheddar_contract, xcheddar_contract) = + init_env(true); + let mut total_reward = 0; + let mut total_locked = 0; + let mut total_supply = 0; + + call!( + owner, + xcheddar_contract.modify_monthly_reward(to_yocto("1").into(), true) + ) + .assert_success(); + let xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(xcheddar_info.monthly_reward.0, to_yocto("1")); + + //add reward trigger distribute_reward, just update prev_distribution_time + call!( + owner, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("100").into(), None, "reward".to_string()), + deposit = 1 + ) + .assert_success(); + total_reward += to_yocto("100"); + + let xcheddar_info1 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(&xcheddar_info1, to_yocto("100"), 0, 0); + assert_eq!(to_yocto("1"), xcheddar_info1.monthly_reward.0); + + //stake trigger distribute_reward + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + total_locked += to_yocto("10"); + total_supply += to_yocto("10"); + + let xcheddar_info2 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = xcheddar_info2.prev_distribution_time_in_sec - xcheddar_info1.prev_distribution_time_in_sec; + total_reward -= time_diff as u128 * xcheddar_info2.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info2.monthly_reward.0; + assert_xcheddar(&xcheddar_info2, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("90"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + assert!(root.borrow_runtime_mut().produce_blocks(10).is_ok()); + + //stake trigger distribute_reward again + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + total_locked += to_yocto("10"); + total_supply += to_yocto("10"); + + let xcheddar_info3 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = xcheddar_info3.prev_distribution_time_in_sec - xcheddar_info2.prev_distribution_time_in_sec; + total_reward -= time_diff as u128 * xcheddar_info3.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info3.monthly_reward.0; + assert_xcheddar(&xcheddar_info3, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("80"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + assert_eq!(xcheddar_info3.undistributed_reward.0, to_yocto("100")); + assert_eq!(xcheddar_info3.locked_token_amount.0, to_yocto("20")); + + assert!(root.borrow_runtime_mut().produce_blocks(10).is_ok()); + + //unstake trigger distribute_reward + call!( + user, + xcheddar_contract.unstake(to_yocto("10").into()), + deposit = 1 + ) + .assert_success(); + + let xcheddar_info4 = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + let time_diff = xcheddar_info4.prev_distribution_time_in_sec - xcheddar_info3.prev_distribution_time_in_sec; + total_reward -= time_diff as u128 * xcheddar_info4.monthly_reward.0; + total_locked += time_diff as u128 * xcheddar_info4.monthly_reward.0; + + let unlocked = (U256::from(to_yocto("10")) * U256::from(total_locked) / U256::from(total_supply)).as_u128(); + total_locked -= unlocked; + total_supply -= to_yocto("10"); + + assert_eq!(to_yocto("10"), total_locked); + assert_eq!(to_yocto("10"), total_supply); + assert_xcheddar(&xcheddar_info4, total_reward, total_locked, total_supply); + assert_eq!(to_yocto("80") + unlocked, view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + assert_eq!(unlocked, to_yocto("10")); + assert_eq!(xcheddar_info4.undistributed_reward.0, to_yocto("100")); + assert_eq!(xcheddar_info4.locked_token_amount.0, to_yocto("10")); +} \ No newline at end of file diff --git a/xcheddar/tests/test_stake.rs b/xcheddar/tests/test_stake.rs new file mode 100644 index 0000000..97e764d --- /dev/null +++ b/xcheddar/tests/test_stake.rs @@ -0,0 +1,65 @@ +use near_sdk_sim::{call, view, to_yocto}; +use xcheddar_token::ContractMetadata; +use near_sdk::json_types::U128; + +mod common; +use crate::common::{ + init::*, + utils::* +}; +//passed +#[test] +fn test_stake(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(true); + + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, to_yocto("10"), to_yocto("10")); + assert_eq!(100000000_u128, view!(xcheddar_contract.get_virtual_price()).unwrap_json::().0); + assert_eq!(to_yocto("90"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); +} + +#[test] +fn test_stake_no_register(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(false); + + let out_come = call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.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("not registered")); + + assert_eq!(to_yocto("100"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, 0, 0); +} + +#[test] +fn test_stake_zero(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(true); + + let out_come = call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("0").into(), None, "".to_string()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("The amount should be a positive number")); + + assert_eq!(to_yocto("100"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, 0, 0); +} \ No newline at end of file diff --git a/xcheddar/tests/test_storage.rs b/xcheddar/tests/test_storage.rs new file mode 100644 index 0000000..da9e92b --- /dev/null +++ b/xcheddar/tests/test_storage.rs @@ -0,0 +1,26 @@ +use near_sdk_sim::{call, view, to_yocto}; +use xcheddar_token::ContractMetadata; + +mod common; +use crate::common::init::*; +//passed +#[test] +fn test_account_number(){ + let (root, _, user, _, xcheddar_contract) = + init_env(true); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(current_xcheddar_info.account_number, 1); + + let user2 = root.create_user("user2".parse().unwrap(), to_yocto("100")); + call!(user2, xcheddar_contract.storage_deposit(None, None), deposit = to_yocto("1")).assert_success(); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(current_xcheddar_info.account_number, 2); + + call!(user2, xcheddar_contract.storage_unregister(None), deposit = 1).assert_success(); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(current_xcheddar_info.account_number, 1); + + call!(user, xcheddar_contract.storage_unregister(None), deposit = 1).assert_success(); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_eq!(current_xcheddar_info.account_number, 0); +} \ No newline at end of file diff --git a/xcheddar/tests/test_unstake.rs b/xcheddar/tests/test_unstake.rs new file mode 100644 index 0000000..6a4193b --- /dev/null +++ b/xcheddar/tests/test_unstake.rs @@ -0,0 +1,96 @@ +use near_sdk_sim::{call, view, to_yocto}; +use xcheddar_token::ContractMetadata; +use near_sdk::json_types::U128; + +mod common; +use crate::common::{ + init::*, + utils::* +}; + +#[test] +fn test_unstake(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(true); + + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + assert_eq!(to_yocto("90"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, to_yocto("10"), to_yocto("10")); + assert_eq!(100000000_u128, view!(xcheddar_contract.get_virtual_price()).unwrap_json::().0); + + call!( + user, + xcheddar_contract.unstake(to_yocto("8").into()), + deposit = 1 + ) + .assert_success(); + + assert_eq!(to_yocto("98"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, to_yocto("2"), to_yocto("2")); + assert_eq!(100000000_u128, view!(xcheddar_contract.get_virtual_price()).unwrap_json::().0); + + let out_come = call!( + user, + xcheddar_contract.unstake(to_yocto("2").into()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("At least 1 Cheddar must be on lockup contract account")); +} + +#[test] +fn test_unstake_empty_total_supply(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(true); + + let out_come = call!( + user, + xcheddar_contract.unstake(to_yocto("1").into()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("Total supply cannot be empty!")); + + assert_eq!(to_yocto("100"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, 0, 0); +} + +#[test] +fn test_unstake_not_enough_balance(){ + let (_, _, user, cheddar_contract, xcheddar_contract) = + init_env(true); + + call!( + user, + cheddar_contract.ft_transfer_call(xcheddar_contract.account_id(), to_yocto("10").into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + assert_eq!(to_yocto("90"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, to_yocto("10"), to_yocto("10")); + assert_eq!(100000000_u128, view!(xcheddar_contract.get_virtual_price()).unwrap_json::().0); + + let out_come = call!( + user, + xcheddar_contract.unstake(to_yocto("11").into()), + deposit = 1 + ); + assert_eq!(get_error_count(&out_come), 1); + assert!(get_error_status(&out_come).contains("The account doesn't have enough balance")); + + assert_eq!(to_yocto("90"), view!(cheddar_contract.ft_balance_of(user.account_id())).unwrap_json::().0); + let current_xcheddar_info = view!(xcheddar_contract.contract_metadata()).unwrap_json::(); + assert_xcheddar(¤t_xcheddar_info, 0, to_yocto("10"), to_yocto("10")); + assert_eq!(100000000_u128, view!(xcheddar_contract.get_virtual_price()).unwrap_json::().0); +} \ No newline at end of file