From 84d9a0eab545c1e1b0f42db519ded3b0b336d249 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Mon, 6 Jun 2022 15:40:35 +0400 Subject: [PATCH 01/12] xCheddar impl add xCheddar lockup token upgrade cheddar to 4.0.0-pre 7 upgate fn migrate() --- Cargo.toml | 1 + README.md | 4 + cheddar/Cargo.toml | 8 +- cheddar/README.md | 11 ++ cheddar/src/internal.rs | 8 +- cheddar/src/lib.rs | 46 +++-- cheddar/src/storage.rs | 24 ++- cheddar/src/upgrade.rs | 81 ++++----- cheddar/src/util.rs | 10 ++ neardev/dev-account | 1 + neardev/dev-account.env | 1 + xcheddar/Cargo.toml | 19 +++ xcheddar/README.md | 165 ++++++++++++++++++ xcheddar/res/cheddar_coin.wasm | Bin 0 -> 238913 bytes xcheddar/res/xcheddar_token.wasm | Bin 0 -> 240744 bytes xcheddar/src/lib.rs | 86 ++++++++++ xcheddar/src/owner.rs | 106 ++++++++++++ xcheddar/src/storage_impl.rs | 49 ++++++ xcheddar/src/utils.rs | 63 +++++++ xcheddar/src/views.rs | 108 ++++++++++++ xcheddar/src/xcheddar.rs | 178 +++++++++++++++++++ xcheddar/tests/common/init.rs | 61 +++++++ xcheddar/tests/common/mod.rs | 2 + xcheddar/tests/common/utils.rs | 39 +++++ xcheddar/tests/test_migrate.rs | 55 ++++++ xcheddar/tests/test_owner.rs | 154 +++++++++++++++++ xcheddar/tests/test_reward.rs | 282 +++++++++++++++++++++++++++++++ xcheddar/tests/test_stake.rs | 65 +++++++ xcheddar/tests/test_storage.rs | 26 +++ xcheddar/tests/test_unstake.rs | 96 +++++++++++ 30 files changed, 1659 insertions(+), 90 deletions(-) create mode 100644 neardev/dev-account create mode 100644 neardev/dev-account.env create mode 100644 xcheddar/Cargo.toml create mode 100644 xcheddar/README.md create mode 100755 xcheddar/res/cheddar_coin.wasm create mode 100755 xcheddar/res/xcheddar_token.wasm create mode 100644 xcheddar/src/lib.rs create mode 100644 xcheddar/src/owner.rs create mode 100644 xcheddar/src/storage_impl.rs create mode 100644 xcheddar/src/utils.rs create mode 100644 xcheddar/src/views.rs create mode 100644 xcheddar/src/xcheddar.rs create mode 100644 xcheddar/tests/common/init.rs create mode 100644 xcheddar/tests/common/mod.rs create mode 100644 xcheddar/tests/common/utils.rs create mode 100644 xcheddar/tests/test_migrate.rs create mode 100644 xcheddar/tests/test_owner.rs create mode 100644 xcheddar/tests/test_reward.rs create mode 100644 xcheddar/tests/test_stake.rs create mode 100644 xcheddar/tests/test_storage.rs create mode 100644 xcheddar/tests/test_unstake.rs diff --git a/Cargo.toml b/Cargo.toml index ce61d77..3c4bbde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ # "./p1-staking-pool-dyn", "./p2-token-staking-fixed", "./p3-farm", + "./xcheddar" ] diff --git a/README.md b/README.md index 3b0da17..7b1e048 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 locked tokens model. After 30-days period distribution of rewards starts and it counting from monthly 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..387ee22 100644 --- a/cheddar/Cargo.toml +++ b/cheddar/Cargo.toml @@ -11,10 +11,10 @@ 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" } +near-sdk = "4.0.0-pre.7" +near-sys = "0.1.0" +near-contract-standards = "4.0.0-pre.7" 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-sdk-sim = "4.0.0-pre.7" diff --git a/cheddar/README.md b/cheddar/README.md index b1c75e7..c041a3d 100644 --- a/cheddar/README.md +++ b/cheddar/README.md @@ -11,3 +11,14 @@ Main features of Cheddar Coin are: ## Technicalities The Cheddar Coin implements the `NEP-141` standard. It's a fungible token. + + +### Compiling + +You can build release version by running next scripts inside each contract folder: + +``` +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 /Users/macbookpro/Documents/GitHub/xCheddar-token/xCheddar/res/cheddar_coin.wasm +``` \ 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..e1646cd 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). @@ -16,19 +15,11 @@ use near_contract_standards::fungible_token::{ }; 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, ext_contract, log, 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 +156,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 +236,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,22 +254,22 @@ 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_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, )) @@ -284,8 +280,8 @@ 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() + fn ft_balance_of(&self, account_id: AccountId) -> U128 { + self._balance_of(&account_id).into() } } @@ -297,8 +293,8 @@ impl FungibleTokenResolver for Contract { #[private] fn ft_resolve_transfer( &mut self, - sender_id: ValidAccountId, - receiver_id: ValidAccountId, + sender_id: AccountId, + receiver_id: AccountId, amount: U128, ) -> U128 { let sender_id: AccountId = sender_id.into(); @@ -347,7 +343,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)) 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..8c622cd 100644 --- a/cheddar/src/util.rs +++ b/cheddar/src/util.rs @@ -1,9 +1,19 @@ use near_sdk::json_types::{U128, U64}; +use near_sdk::{Balance, 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); +pub const NO_DEPOSIT: Balance = 0; + construct_uint! { /// 256-bit unsigned integer. pub struct U256(4); diff --git a/neardev/dev-account b/neardev/dev-account new file mode 100644 index 0000000..ba8ffce --- /dev/null +++ b/neardev/dev-account @@ -0,0 +1 @@ +dev-1654515440352-14631167054094 \ No newline at end of file diff --git a/neardev/dev-account.env b/neardev/dev-account.env new file mode 100644 index 0000000..5c317de --- /dev/null +++ b/neardev/dev-account.env @@ -0,0 +1 @@ +CONTRACT_NAME=dev-1654515440352-14631167054094 \ No newline at end of file diff --git a/xcheddar/Cargo.toml b/xcheddar/Cargo.toml new file mode 100644 index 0000000..c1a1ec5 --- /dev/null +++ b/xcheddar/Cargo.toml @@ -0,0 +1,19 @@ +[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-sdk = "4.0.0-pre.7" +near-sys = "0.1.0" +near-contract-standards = "4.0.0-pre.7" +chrono = "0.4.19" + +[dev-dependencies] +near-sdk-sim = "4.0.0-pre.7" +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..34407dc --- /dev/null +++ b/xcheddar/README.md @@ -0,0 +1,165 @@ +# XCheddar Token Contract + +### Sumary +* Stake CHEDDAR token to lock in the contract and get XCHEDDAR on price P, +XCHEDDAR_amount = staked_CHEDDAR / P, +where P = locked_CHEDDAR_token_amount / XCHEDDAR_total_supply. + +* Redeem CHEDDAR by unstake using XCHEDDAR token on price P, +redeemed_CHEDDAR = unstaked_XCHEDDAR * P, +where P = 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` per second 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 +export ONE_CHEDDAR=1000000000000000000000000 +export FIVE_CHEDDAR=5000000000000000000000000 +export EIGHT_CHEDDAR=8000000000000000000000000 + +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=$USER_ACCOUNT --depositYocto=1 --gas=$GAS +``` + +#### owner reset reward genesis time +```bash +near call $XCHEDDAR_TOKEN get_owner '' --account_id=$XCHEDDAR_OWNER +# set to 2022-06-06 00:00:00 GMT time +near call $XCHEDDAR_TOKEN reset_reward_genesis_time_in_sec '{"reward_genesis_time_in_sec": 1654438300}' --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_month to 5 CHEDDAR +```bash +near call $XCHEDDAR_TOKEN modify_monthly_reward '{"monthly_reward": "'$FIVE_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_month` at call time before changing to the new one. + +#### unstake 8 XCHEDDAR get CHEDDAR and reward back +```bash +near call $XCHEDDAR_TOKEN unstake '{"amount": "'$EIGHT_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 0000000000000000000000000000000000000000..6b89263236844499cd673abe0480f9a32c534b61 GIT binary patch literal 238913 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≦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 literal 0 HcmV?d00001 diff --git a/xcheddar/res/xcheddar_token.wasm b/xcheddar/res/xcheddar_token.wasm new file mode 100755 index 0000000000000000000000000000000000000000..68729d2a5f4831a1e01c41509dce25a5eef10098 GIT binary patch literal 240744 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⁣D^>2Qn*4B$G~hXV^)nmrZ0{w27e8ua8&nxzzKAO z-32Nq+M^%EXu2_n=xMzMg_Ak4%xuTDUsi*G2MJh71jC*g$WCROp)tBuWo3ij80hIU z#vGZIo-88+?81SPzQR-(;($!(Apf-Z^gOimO94#+7*8w{CF7Du!de_{5y5H|?CLAW@mC?@sHF1&;fh+myj*ZLv}>6!JY~YXkR)zw6TPc6k4GqO*}Rm%#gIo3ZxPV za8CytWcU7Cq_}2_lko+EA*(fwX$! zL1_b83Cm5%bm(X>Dg%!NiVXb=p0iRS$V?*p1}8xliD2iBHp>8PLEHg*j3DzsWk7k; z!WUY;02WdcF>F4xebhe@EdT;5PEs)GL;EB70vbye0u6=O0a7HBVbUOfM2mnH1F`*Y z3An|k8K6^!v|+?%Z2!uPV#b4fxT3uN#$0hEW2%cWGe+xnXU2ngNIV)&12Bea<3WxD zK8(%R(Wr4?#~>fHc4X~rNM(tPULYG0CWDO-2hB6kycmr)EcSqPU{@vV2^h&Kl*@_c zK&bB6I$8$q@L?zXoV6 zP^j`)6@av_0csH zLys>Sd4`~oM@TBJ`-xB%#f`y1{&%Pl_fdSuLauDI)<$Wf41!)9WZ*Jf~&IsMg@io zJ*GQ<7ryB`yczHopshgdSC306y@rd>Unwd(e*Y1Y~ussbORWTyMns^408J$>xYJ$EKmZ8Nsl?i_j5wZB?O z%gE!>(2KeaUnGB+R)&5$dryAv_w@HQ4fkR{1#?>4$v0JNrf{!M}dDdVZ z9!zXO#kYTKiB>RQm5B$4Z7{=;8uU6o8}6TO-|b{y5IH$LTS~`&D!$J{ZILTLGy`Q|l zwK%;GQL4pG8?f*XyO zY&Z@Ly^j(W0#@dSWE;vD$ug8Nl65F!weRpyz-TVS<|hHHoc~zB=$*iZj{~fHzmox@ z`$NAvcYPPKR{z%%UoSQze75K$t@plOqjA8!%A<|Da#}uqy+>owjg-L+bHk2m zHrCU)e`DdCZCx+TvS@rouPvgDkHf$@c7`r0Av603XX1?y8)2-}9 zek0X_@=FWO9W>Yb;`T+2A{vL+?2F0k(0|gdMlBjUgnjk1U!ZaFLZcy#1D1F>X1rK5 z=hsGK8lQEV)Z@Yw*-fm;g2wL`A6xuO?bLFECOaBGtuWVJBD8Dh+ti=Nvw!Yu{&ke& z&w)*DG)Pv0sWk5S_SA#wgHG!C{oVHej@d2V2H35`R09-3P+#eY#`^8y+lnP`@2ob#nl)?7y8(gRNqtU6M+ zp|p7&jTaQirq{-9{=B)lg2uJWHDSjVlWCFI7DrxKyjQK`Tkg?V?L0R5kG<2H zMz++`_@%z)yhQ%f%84znXnb*cV20pgWy#W(_cXSU=sg~;yZ`d;mPQ(*XAYxzSxrmZ zK&`R<+VkfvXx4&$2^D8v=58bDHvq_+1q(js_8VB#g?@vAt#xhQx%`^N#~l^c@H3-d z}ZPhYcLGxGN}m>Zqj zrft0Pfo+KrEAmHxZ*MzPf01*lbJWQqTyyldkl;v$Xff{pUuMd~amBCgTc$)@Y#dwa_tFDLHNc+u`B(ZQE*Z`w!H z)40Fe#&x$REdP3mctv9kw{;tRea9VsNxY}Ae%YXX4YRk-aWW+o`sS~LJZ;WofB@0O)D}4P&fHZ z(wN4U_bbRj^+w0mlNK~CZE+i6T{dd=ang>)gAbIHtJFtJTl7+$@}=<)r#bE3y`!>{rP=hHXUP~1+7={QucG-gi#LNCjo~atb1Y?y z=3B}bJw0W-?K`|(f&cPl-%HHAS;hUurnch#{l)gS@M>+%f5&(H&^rjNJJ_Gne#y7r z-+>5h8PIzOy$4yW3)oc&5BV~yLIeSQJ; zz8{whsv^oJ2Bu#L(Iz(0FePqHbL216fD{kDK+`&tv1?0nRFTHU%F41AxNVZtll*-vRYG&#Z4z#h6c|79Zv%pF;p5@7GNTR56HoQ2V5}2 z491=d%9~Qqez+>*1At)(3$t#i5m>5g|Lo&T>92c;>2G$J#R)bM zPGaDb0{J1?L9rs=MQ{}XUISNDrZ}L#ic<}7P<?Vxg7vg@LUwX0?A@WmgNB(*U06eWBOn&rfE&qC#Q8!Cp@mqSFD?)lmfWHC6kpO7{+2sEJa>A#5Q!q!L&Va$kwjZpa73F>k4-Jh~%5{du zA~d!f1NvdrDaO0Xu}t0Ba2fe5(lY!fKbD_4@Uw)xTfcn(ZZ>Qx2(!8<;aPx@49nE4 z-JQ|`7i-uv0&BGN`#UL%X-iQMAC;N;DhzCnWT7+FsGqVOl?g$^AI$`*ZbE-WU!_?B zJW#va0@rS>Jdk=P!{HwIqY;D+XVNoabWZ5hE7R*C5e=|LNQ27ILCWko!gt6rkb!B~ z>B*KQSvt~R-vhlBW*3nWxicK0y+RsuNXw3Y z8kbnC5A9f63448qy%jhLOq9mZd(_PHLGJ}*7zZ96^Tt4~IMCt9X7+Gw;ELJ{JLV>U zErE1#e-~QI*c3U;7)27)_>gQ9(#Am=B>zo^D=K>yTv0n^d7wPg>FqeQi_GK;dQr}_ zy#aK1T#CNaC^lt21ZhU!5qm%DfLBl8bsnzJ9kDBLg`&0c{$c$p#t>{AGlqPC_?>~; z#Z_n`TZ2J~bb6na?#h@2r0W6kk&Mw0h{dQ~+XG=ZiR{x7JT9Ln&=eY~=!x`IHPlqq zDGd^g&LU8)nVN9ywoa6_W2j+pF4TB=y#Hl!`l8{bH5Cbm#p`F|2$IZZ?}*&^7M zn>TaT8k_LpGfUif0P%6edQOzh+0S+nQP+q~=0;WJg|&OfMs+JaG<9n5U) z9Gsk8ynSMdXG7qgLx-!**VNWO#VA!ZI@HP8-NV~wM53g4?(!AqYHC$A&A`h$JSuub zY@%e=+|7{W%(=Syr*Bj>-Mtef^5UN>j~=^z^Ud4IQ)aH)aP-)jvo*DM?s_db@=H}s zt+#(bc*Ka<8M8}w?c0CsMAg}wnmW2s(H}oGx5(4RKB!acnx0|UIksTp_8oYU#)iq44UpJ&@IJgXSb)PdgcvN=vxl5OC-G0{G0w)){O{$|Nx$zAsu4Yk%ntUs# ztDwk$)Z^onHDyQfNSw#zX$k|?JMcnzBxNWRkbIIyB7?#ribJY!aW!p@KhJ;{&Lg-w zs)5uH(vrj}O|H7giR#=VR-8tS=^?M?Oxj86b0;;C5j-71CxMnoOEiWn|biWNqt5%GdGxlj>xDo+G~(XHtu1C%(0&C26E?q%JR^CN0)e z(Vo75W6g2q5o(CwDRw>Ii%(`A^)f>qNSg?P{v%PEVeQ!;x>$DEh#|#pYZHokhuyk_M!@ zD#nFxQR5WB;qeHtolFSf6YXl02ChkT;B?f~!nFxqLQmC@)0uCKkD*eC9b_d@OI#+d ziLMK75I2cC_(RSk;u-avcrAWIeI}rx;vy600saA{D_5?{pE-Zgnx79%-Noez>|F+g zy}NXo(&}XI5EfpzW&4gJeIIt1He>e6)|QAGqJKc5BzoU|14ABPsG_B7-_L2&=3BP~ z4s+&i;t8DxBqf*5%ZNSpx*;;*ePheAONWQ>EhhszKWv3$~V{1?`f!|EAWR5+*I{=LarCzM3AjAaIiVonIq&5qpN(}{x@Ka_tm^QD9oFx%LwV(vh?z!@1lW#$xRY{pBf(N}wT|_xTVNChm{EI?F+9ZeM2|AN19IjBv<-@2d zztBTi%>By&TQP9QBE1x5%)XxwdxbDGqxChqvN#;v=zYfGAaD-=YR4LFLnS1SVrK5J z4?01Eo(opM92TuT+Ru2?Ghp8rz7NtqgkE(@L4aas(>{>VJVIM4-^`1TLmnYnd1$*X z7;Wa!>&sx+*gytbY0$ch2RdBIiLqcSQI;V=iWo?@_#0j)fEQX+O{T=yTuvMo)3F@u zpeybw632CIuqf|kZX>qH*znMT*c4~k&!g+>m>s*fDDs>S!S z?zFm}jo~BDcU_Ep-;8+_5RhRMxT11(AXXbcOmcbkFzk*|5ccp`hh2L9 zC`^o{hrhvF!ojct53__B9sz&2mx_(H1}=fYmmqMeJKn`0O2tVaz&la60LCg#FS0-1 zyb~^VfCLmD23ekv=!`p|b0*Cc5F{NQw~PT=Fhd0^BVRaU}R$A%WX;wWVwTb8u6Eh!8^|ancb2AtC9+Cx|5^ zu8Q+eh9prn2*XbqVdN}4PK#pVj_@$Sqa>`9>KvE6Z#!g@t%A=6)VyP%0>_- z_zJX4 zJSP+jCtkvRap7t}gz}Nk7*rP!Dk0&4axei-e1NA8tpT3{xly=SXv(FZ5|<#Ypt3O@ zR0bZT12+O(#&aP9sBdU#C@UPw0qAQ!gy&sEKdH0+51BCMl}CqsSn6cbb#J~EayDXI-vgnvmvg) literal 0 HcmV?d00001 diff --git a/xcheddar/src/lib.rs b/xcheddar/src/lib.rs new file mode 100644 index 0000000..f85a8f9 --- /dev/null +++ b/xcheddar/src/lib.rs @@ -0,0 +1,86 @@ +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::json_types::U128; + +#[allow(unused_imports)] +use near_sdk::{env, log, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, PromiseOrValue}; +use crate::utils::*; +pub use crate::views::ContractMetadata; + +mod xcheddar; +mod utils; +mod owner; +mod views; +mod storage_impl; + +#[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, + /// 30-days period reward + pub monthly_reward: 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, + monthly_reward: 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..ced4d9d --- /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 modify_monthly_reward(&mut self, monthly_reward: U128, distribute_before_change: bool) { + self.assert_owner(); + if distribute_before_change { + self.distribute_reward(); + } + self.monthly_reward = monthly_reward.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..b3fea64 --- /dev/null +++ b/xcheddar/src/utils.rs @@ -0,0 +1,63 @@ +use chrono::prelude::*; +use near_sdk::json_types::U128; +use near_sdk::{ext_contract, Balance, Gas, Timestamp}; + +use uint::construct_uint; + +pub const CHEDDAR_DECIMALS: u8 = 24; +pub const NO_DEPOSIT: u128 = 0; +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"; + +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) +} + +/// U can impl this function from cheddar vesting locking to calculate minted and unlocked in xcheddar.rs +/// This contract of xCheddar using same logic to count amounts of locked tokens +/* +///returns amount * numerator/denominator +pub fn fraction_of(amount: u128, numerator: u128, denominator: u128) -> u128 { + return (U256::from(amount) * U256::from(numerator) / U256::from(denominator)).as_u128(); +} + */ + + +//callbacks +#[ext_contract(ext_self)] +pub trait XCheddar { + fn callback_post_unstake( + &mut self, + sender_id: AccountId, + amount: U128, + share: U128, + ); +} + + diff --git a/xcheddar/src/views.rs b/xcheddar/src/views.rs new file mode 100644 index 0000000..d97b8ac --- /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 monthly_reward: 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 monthly_reward: 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, + monthly_reward: self.monthly_reward.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), + monthly_reward: convert_from_yocto_cheddar(self.monthly_reward).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..e058677 --- /dev/null +++ b/xcheddar/src/xcheddar.rs @@ -0,0 +1,178 @@ + +use crate::*; +#[allow(unused_imports)] +use crate::utils::*; +use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +use near_contract_standards::fungible_token::core_impl::ext_fungible_token; +use near_sdk::json_types::U128; +use near_sdk::{assert_one_yocto, env, log, Promise, PromiseResult}; +use std::cmp::{max, min}; + +impl Contract { + pub 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); + + 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); + + self.locked_token_amount += amount; + + 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) + ); + } + + pub 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) + //reward_per_month = reward_per_sec * DURATION_30_DAYS_IN_SEC + let ideal_amount = self.monthly_reward * ((cur_timestamp_in_sec - self.prev_distribution_time_in_sec) / DURATION_30DAYS_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); + } +} + +#[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(); + + self.ft.internal_withdraw(&account_id, amount); + assert!(self.ft.total_supply >= 10u128.pow(24), "{}", ERR_KEEP_AT_LEAST_ONE_XCHEDDAR); + self.locked_token_amount -= unlocked; + + log!("Withdraw {} (~{} Cheddar) from @{}", amount, convert_from_yocto_cheddar(amount), account_id); + + 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, + )) + } + + #[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); + if msg.is_empty() { + // user stake + self.internal_stake(&sender_id, amount); + PromiseOrValue::Value(U128(0)) + } else { + // deposit reward + log!("Add reward {} token with msg {}", amount, msg); + self.internal_add_reward(&sender_id, amount); + PromiseOrValue::Value(U128(0)) + } + } +} \ 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..6e85fed --- /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_befroe_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 From 1b4a637ee4ba6395e6698d555f341238b6147298 Mon Sep 17 00:00:00 2001 From: Oilbird Date: Fri, 10 Jun 2022 21:34:47 +0400 Subject: [PATCH 02/12] Add link to Curve resources --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b1e048..7758362 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,4 @@ A Defi token and farm on NEAR. Cheddar is a fun way for NEAR users to collect, s ## 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 locked tokens model. After 30-days period distribution of rewards starts and it counting from monthly reward parameter. Locked token have a virtual price depends on amount of locked/minted XCheddar tokens +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 monthly reward parameter. Locked token have a virtual price depends on amount of locked/minted XCheddar tokens From 0bbc2532016e9091e02a597a333df623752949d3 Mon Sep 17 00:00:00 2001 From: Oilbird Date: Fri, 10 Jun 2022 21:37:19 +0400 Subject: [PATCH 03/12] Delete neardev directory --- neardev/dev-account | 1 - neardev/dev-account.env | 1 - 2 files changed, 2 deletions(-) delete mode 100644 neardev/dev-account delete mode 100644 neardev/dev-account.env diff --git a/neardev/dev-account b/neardev/dev-account deleted file mode 100644 index ba8ffce..0000000 --- a/neardev/dev-account +++ /dev/null @@ -1 +0,0 @@ -dev-1654515440352-14631167054094 \ No newline at end of file diff --git a/neardev/dev-account.env b/neardev/dev-account.env deleted file mode 100644 index 5c317de..0000000 --- a/neardev/dev-account.env +++ /dev/null @@ -1 +0,0 @@ -CONTRACT_NAME=dev-1654515440352-14631167054094 \ No newline at end of file From 118321259c10d029aa74704f0a6c0e669b10a7c1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 21 Jun 2022 14:09:03 +0200 Subject: [PATCH 04/12] Update cheddar/README.md --- cheddar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cheddar/README.md b/cheddar/README.md index c041a3d..72f6282 100644 --- a/cheddar/README.md +++ b/cheddar/README.md @@ -20,5 +20,5 @@ You can build release version by running next scripts inside each contract folde ``` 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 /Users/macbookpro/Documents/GitHub/xCheddar-token/xCheddar/res/cheddar_coin.wasm +cp target/wasm32-unknown-unknown/release/cheddar_coin.wasm res/cheddar_coin.wasm ``` \ No newline at end of file From 5b3d5a52423b8702b2003d8eb185d458d811a33c Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Sun, 26 Jun 2022 16:42:54 +0400 Subject: [PATCH 05/12] v4.0.0 --- .gitignore | 3 ++- cheddar/Cargo.toml | 9 +++------ cheddar/README.md | 13 +------------ xcheddar/Cargo.toml | 5 ++--- xcheddar/README.md | 10 +++++----- xcheddar/tests/test_reward.rs | 2 +- 6 files changed, 14 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 7107c9f..92d0b64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ # Generated by Cargo # will have compiled files and executables /target/ - +/xcheddar/res/ /res + # 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 Cargo.lock diff --git a/cheddar/Cargo.toml b/cheddar/Cargo.toml index 387ee22..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 = "4.0.0-pre.7" -near-sys = "0.1.0" -near-contract-standards = "4.0.0-pre.7" uint = { version = "0.9.0", default-features = false } - -[dev-dependencies] -near-sdk-sim = "4.0.0-pre.7" +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 72f6282..fe9dba9 100644 --- a/cheddar/README.md +++ b/cheddar/README.md @@ -10,15 +10,4 @@ Main features of Cheddar Coin are: ## Technicalities -The Cheddar Coin implements the `NEP-141` standard. It's a fungible token. - - -### Compiling - -You can build release version by running next scripts inside each contract folder: - -``` -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 res/cheddar_coin.wasm -``` \ No newline at end of file +The Cheddar Coin implements the `NEP-141` standard. It's a fungible token. \ No newline at end of file diff --git a/xcheddar/Cargo.toml b/xcheddar/Cargo.toml index c1a1ec5..eea3f7a 100644 --- a/xcheddar/Cargo.toml +++ b/xcheddar/Cargo.toml @@ -9,11 +9,10 @@ crate-type = ["cdylib", "rlib"] [dependencies] uint = { version = "0.9.0", default-features = false } -near-sdk = "4.0.0-pre.7" near-sys = "0.1.0" -near-contract-standards = "4.0.0-pre.7" +near-contract-standards = "4.0.0" +near-sdk = "4.0.0" chrono = "0.4.19" [dev-dependencies] -near-sdk-sim = "4.0.0-pre.7" cheddar-coin = {path = "../cheddar"} \ No newline at end of file diff --git a/xcheddar/README.md b/xcheddar/README.md index 34407dc..a028475 100644 --- a/xcheddar/README.md +++ b/xcheddar/README.md @@ -2,15 +2,15 @@ ### Sumary * Stake CHEDDAR token to lock in the contract and get XCHEDDAR on price P, -XCHEDDAR_amount = staked_CHEDDAR / P, -where P = locked_CHEDDAR_token_amount / XCHEDDAR_total_supply. +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, -where P = locked_CHEDDAR_token_amount / XCHEDDAR_total_supply. +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` per second after `reward_genesis_time_in_sec`. +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. diff --git a/xcheddar/tests/test_reward.rs b/xcheddar/tests/test_reward.rs index 6e85fed..1435612 100644 --- a/xcheddar/tests/test_reward.rs +++ b/xcheddar/tests/test_reward.rs @@ -185,7 +185,7 @@ fn test_reward() { } #[test] -fn test_no_reward_befroe_reset_reward_genesis_time(){ +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; From 976df7ea7f6d2e69b213ec3490bb607d5e57d0a2 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Sun, 26 Jun 2022 16:43:32 +0400 Subject: [PATCH 06/12] traits update --- cheddar/src/lib.rs | 38 ++++++++++++++------------------------ xcheddar/src/lib.rs | 8 ++++++-- xcheddar/src/owner.rs | 2 +- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/cheddar/src/lib.rs b/cheddar/src/lib.rs index e1646cd..2a1da37 100644 --- a/cheddar/src/lib.rs +++ b/cheddar/src/lib.rs @@ -11,7 +11,7 @@ use near_contract_standards::fungible_token::{ core::FungibleTokenCore, metadata::{FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC}, - resolver::FungibleTokenResolver, + core_impl::{ext_ft_receiver, ext_ft_resolver} }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; @@ -256,7 +256,8 @@ impl FungibleTokenCore for Contract { let amount: Balance = amount.into(); 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(), @@ -273,6 +274,15 @@ impl FungibleTokenCore for Contract { 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() } @@ -314,26 +324,6 @@ impl FungibleTokenMetadataProvider for Contract { } } -#[ext_contract(ext_ft_receiver)] -pub trait FungibleTokenReceiver { - fn ft_on_transfer( - &mut self, - sender_id: AccountId, - amount: U128, - msg: String, - ) -> PromiseOrValue; -} - -#[ext_contract(ext_self)] -trait FungibleTokenResolver { - fn ft_resolve_transfer( - &mut self, - sender_id: AccountId, - receiver_id: AccountId, - amount: U128, - ) -> U128; -} - #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use near_sdk::test_utils::{accounts, VMContextBuilder}; @@ -362,7 +352,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); @@ -387,7 +377,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/xcheddar/src/lib.rs b/xcheddar/src/lib.rs index f85a8f9..b8a2ac3 100644 --- a/xcheddar/src/lib.rs +++ b/xcheddar/src/lib.rs @@ -5,9 +5,8 @@ use near_contract_standards::fungible_token::FungibleToken; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::json_types::U128; +use near_sdk::{env, log, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, PromiseOrValue}; -#[allow(unused_imports)] -use near_sdk::{env, log, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, PromiseOrValue}; use crate::utils::*; pub use crate::views::ContractMetadata; @@ -17,6 +16,11 @@ 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 { diff --git a/xcheddar/src/owner.rs b/xcheddar/src/owner.rs index ced4d9d..47d398a 100644 --- a/xcheddar/src/owner.rs +++ b/xcheddar/src/owner.rs @@ -75,7 +75,7 @@ mod upgrade { env::setup_panic_hook(); - ///assert ownership + /// assert ownership let contract: Contract = env::state_read().expect("ERR_CONTRACT_IS_NOT_INITIALIZED"); contract.assert_owner(); From 428f503dc3d64c05daf2c995d5815791923a7c2c Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Sun, 26 Jun 2022 16:43:44 +0400 Subject: [PATCH 07/12] callbacks update --- xcheddar/src/utils.rs | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/xcheddar/src/utils.rs b/xcheddar/src/utils.rs index b3fea64..80ec6ef 100644 --- a/xcheddar/src/utils.rs +++ b/xcheddar/src/utils.rs @@ -1,11 +1,10 @@ use chrono::prelude::*; -use near_sdk::json_types::U128; -use near_sdk::{ext_contract, Balance, Gas, Timestamp}; +use near_sdk::{Balance, Gas, Timestamp}; use uint::construct_uint; pub const CHEDDAR_DECIMALS: u8 = 24; -pub const NO_DEPOSIT: u128 = 0; +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); @@ -21,6 +20,7 @@ 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. @@ -37,27 +37,5 @@ pub fn convert_from_yocto_cheddar(yocto_amount: Balance) -> u128 { pub fn convert_timestamp_to_datetime(timestamp: u32) -> DateTime { let naive_datetime = NaiveDateTime::from_timestamp(timestamp.into(), 0); DateTime::from_utc(naive_datetime, Utc) -} - -/// U can impl this function from cheddar vesting locking to calculate minted and unlocked in xcheddar.rs -/// This contract of xCheddar using same logic to count amounts of locked tokens -/* -///returns amount * numerator/denominator -pub fn fraction_of(amount: u128, numerator: u128, denominator: u128) -> u128 { - return (U256::from(amount) * U256::from(numerator) / U256::from(denominator)).as_u128(); } - */ - - -//callbacks -#[ext_contract(ext_self)] -pub trait XCheddar { - fn callback_post_unstake( - &mut self, - sender_id: AccountId, - amount: U128, - share: U128, - ); -} - From b03eecb4f4db1dd041bf815dacce5df8a9d9ad50 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Sun, 26 Jun 2022 16:43:56 +0400 Subject: [PATCH 08/12] add transfer call instructions --- xcheddar/src/xcheddar.rs | 75 +++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/xcheddar/src/xcheddar.rs b/xcheddar/src/xcheddar.rs index e058677..cfb3344 100644 --- a/xcheddar/src/xcheddar.rs +++ b/xcheddar/src/xcheddar.rs @@ -1,18 +1,32 @@ - use crate::*; #[allow(unused_imports)] -use crate::utils::*; +use crate::utils::convert_from_yocto_cheddar; use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; -use near_contract_standards::fungible_token::core_impl::ext_fungible_token; 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 fn internal_stake(&mut self, account_id: &AccountId, amount: Balance) { + 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 { @@ -22,8 +36,9 @@ impl Contract { 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, @@ -32,9 +47,11 @@ impl Contract { minted, convert_from_yocto_cheddar(minted) ); + // total_supply += amount * P, where P<=1 + // locked_token_amount += amount } - pub fn internal_add_reward(&mut self, account_id: &AccountId, amount: Balance) { + 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)); } @@ -57,8 +74,11 @@ impl Contract { 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"); } - self.prev_distribution_time_in_sec = max(cur_time, self.reward_genesis_time_in_sec); } } @@ -84,12 +104,16 @@ impl Contract { 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), @@ -106,6 +130,17 @@ impl Contract { 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] @@ -164,15 +199,21 @@ impl FungibleTokenReceiver for Contract { let token_in = env::predecessor_account_id(); let amount: Balance = amount.into(); assert_eq!(token_in, self.locked_token, "{}", ERR_MISMATCH_TOKEN); - if msg.is_empty() { - // user stake - self.internal_stake(&sender_id, amount); - PromiseOrValue::Value(U128(0)) - } else { - // deposit reward - log!("Add reward {} token with msg {}", amount, msg); - self.internal_add_reward(&sender_id, amount); - PromiseOrValue::Value(U128(0)) + 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)) + } } } } \ No newline at end of file From 1fad008457f1390063742074f3d0c59549ef2c89 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Thu, 30 Jun 2022 13:31:42 +0400 Subject: [PATCH 09/12] modify reward to reward_per_sec --- .gitignore | 3 ++- Cargo.toml | 3 ++- README.md | 2 +- cheddar/src/lib.rs | 47 +++++++++++++++++++++++++++++-------------- cheddar/src/util.rs | 3 +-- p3-farm/Cargo.toml | 1 - p4-pool/Cargo.toml | 18 +++++++++++++++++ xcheddar/README.md | 19 ++++++++--------- xcheddar/src/lib.rs | 10 +++++---- xcheddar/src/owner.rs | 4 ++-- xcheddar/src/views.rs | 8 ++++---- 11 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 p4-pool/Cargo.toml diff --git a/.gitignore b/.gitignore index 92d0b64..0ac33e3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ /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 3c4bbde..de5ea1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,8 @@ members = [ # "./p1-staking-pool-dyn", "./p2-token-staking-fixed", "./p3-farm", - "./xcheddar" + "./xcheddar", + "./p4-pool" ] diff --git a/README.md b/README.md index 7758362..dd7f691 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,4 @@ A Defi token and farm on NEAR. Cheddar is a fun way for NEAR users to collect, s ## 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 monthly reward parameter. Locked token have a virtual price depends on amount of locked/minted XCheddar tokens +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/src/lib.rs b/cheddar/src/lib.rs index 2a1da37..2b5f351 100644 --- a/cheddar/src/lib.rs +++ b/cheddar/src/lib.rs @@ -10,14 +10,13 @@ /// use near_contract_standards::fungible_token::{ core::FungibleTokenCore, - metadata::{FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC}, - core_impl::{ext_ft_receiver, ext_ft_resolver} + metadata::{FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC}, }; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap}; use near_sdk::json_types::U128; use near_sdk::{ - assert_one_yocto, env, ext_contract, log, near_bindgen, AccountId, Balance, + assert_one_yocto, env, log, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue, }; mod internal; @@ -294,27 +293,45 @@ impl FungibleTokenCore for Contract { 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, + amount: U128, + msg: String, + ) -> PromiseOrValue; +} -#[near_bindgen] -impl FungibleTokenResolver for Contract { +#[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) - #[private] fn ft_resolve_transfer( &mut self, sender_id: AccountId, receiver_id: AccountId, 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(); - } + ) -> U128; } #[near_bindgen] diff --git a/cheddar/src/util.rs b/cheddar/src/util.rs index 8c622cd..773af06 100644 --- a/cheddar/src/util.rs +++ b/cheddar/src/util.rs @@ -1,5 +1,5 @@ use near_sdk::json_types::{U128, U64}; -use near_sdk::{Balance, Gas}; +use near_sdk::Gas; use uint::construct_uint; pub type U128String = U128; @@ -12,7 +12,6 @@ pub const ONE_TERA: Gas = Gas(1_000_000_000_000); 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); -pub const NO_DEPOSIT: Balance = 0; construct_uint! { /// 256-bit unsigned integer. diff --git a/p3-farm/Cargo.toml b/p3-farm/Cargo.toml index e8c7d42..4e570d0 100644 --- a/p3-farm/Cargo.toml +++ b/p3-farm/Cargo.toml @@ -9,7 +9,6 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] - serde = { version = "*", features = ["derive"] } serde_json = "*" uint = { version = "0.9.0", default-features = false } 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/README.md b/xcheddar/README.md index a028475..607bc8f 100644 --- a/xcheddar/README.md +++ b/xcheddar/README.md @@ -98,9 +98,11 @@ 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 EIGHT_CHEDDAR=8000000000000000000000000 +export TEN_CHEDDAR=10000000000000000000000000 near call $XCHEDDAR_TOKEN new '{"owner_id": "'$XCHEDDAR_OWNER'", "locked_token": "'$CHEDDAR_TOKEN'"}' --account_id=$XCHEDDAR_TOKEN ``` @@ -142,24 +144,23 @@ near call $CHEDDAR_TOKEN ft_transfer_call '{"receiver_id": "'$XCHEDDAR_TOKEN'", #### 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=$USER_ACCOUNT --depositYocto=1 --gas=$GAS +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 -# set to 2022-06-06 00:00:00 GMT time -near call $XCHEDDAR_TOKEN reset_reward_genesis_time_in_sec '{"reward_genesis_time_in_sec": 1654438300}' --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_month to 5 CHEDDAR +#### owner modify reward_per_second to 5 CHEDDAR ```bash -near call $XCHEDDAR_TOKEN modify_monthly_reward '{"monthly_reward": "'$FIVE_CHEDDAR'", "distribute_before_change": true}' --account_id=$XCHEDDAR_OWNER --gas=$GAS +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_month` at call time before changing to the new one. +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 8 XCHEDDAR get CHEDDAR and reward back +#### unstake 10 XCHEDDAR get CHEDDAR and reward back ```bash -near call $XCHEDDAR_TOKEN unstake '{"amount": "'$EIGHT_CHEDDAR'"}' --account_id=$USER_ACCOUNT --depositYocto=1 --gas=$GAS +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/src/lib.rs b/xcheddar/src/lib.rs index b8a2ac3..e5ee924 100644 --- a/xcheddar/src/lib.rs +++ b/xcheddar/src/lib.rs @@ -4,8 +4,9 @@ use near_contract_standards::fungible_token::metadata::{ 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, log, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, PromiseOrValue}; +use near_sdk::{env, ext_contract, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue}; use crate::utils::*; pub use crate::views::ContractMetadata; @@ -35,8 +36,9 @@ pub struct Contract { pub prev_distribution_time_in_sec: u32, /// when would the reward starts to distribute pub reward_genesis_time_in_sec: u32, - /// 30-days period reward - pub monthly_reward: Balance, + /// 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, } @@ -56,7 +58,7 @@ impl Contract { locked_token_amount: 0, prev_distribution_time_in_sec: initial_reward_genisis_time, reward_genesis_time_in_sec: initial_reward_genisis_time, - monthly_reward: 0, + reward_per_second: 0, account_number: 0, } } diff --git a/xcheddar/src/owner.rs b/xcheddar/src/owner.rs index 47d398a..a195bfe 100644 --- a/xcheddar/src/owner.rs +++ b/xcheddar/src/owner.rs @@ -18,12 +18,12 @@ impl Contract { self.owner_id.clone() } - pub fn modify_monthly_reward(&mut self, monthly_reward: U128, distribute_before_change: bool) { + 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.monthly_reward = monthly_reward.into(); + self.reward_per_second = reward_per_second.into(); } pub fn reset_reward_genesis_time_in_sec(&mut self, reward_genesis_time_in_sec: u32) { diff --git a/xcheddar/src/views.rs b/xcheddar/src/views.rs index d97b8ac..4e83e0c 100644 --- a/xcheddar/src/views.rs +++ b/xcheddar/src/views.rs @@ -22,7 +22,7 @@ pub struct ContractMetadata { pub supply: U128, pub prev_distribution_time_in_sec: u32, pub reward_genesis_time_in_sec: u32, - pub monthly_reward: U128, + pub reward_per_second: U128, /// current account number in contract pub account_number: u64, } @@ -45,7 +45,7 @@ pub struct ContractMetadataHumanReadable { pub supply: U128, pub prev_distribution_time: DateTime, pub reward_genesis_time: DateTime, - pub monthly_reward: U128, + pub reward_per_second: U128, /// current account number in contract pub account_number: u64, } @@ -68,7 +68,7 @@ impl Contract { 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, - monthly_reward: self.monthly_reward.into(), + reward_per_second: self.reward_per_second.into(), account_number: self.account_number, } } @@ -88,7 +88,7 @@ impl Contract { 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), - monthly_reward: convert_from_yocto_cheddar(self.monthly_reward).into(), + reward_per_second: convert_from_yocto_cheddar(self.reward_per_second).into(), account_number: self.account_number, } } From d2d86b376c1536d0735e576c234846b6f03a484b Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Thu, 30 Jun 2022 13:31:54 +0400 Subject: [PATCH 10/12] add P tests --- xcheddar/src/xcheddar.rs | 99 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/xcheddar/src/xcheddar.rs b/xcheddar/src/xcheddar.rs index cfb3344..8440255 100644 --- a/xcheddar/src/xcheddar.rs +++ b/xcheddar/src/xcheddar.rs @@ -60,8 +60,7 @@ impl Contract { 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) - //reward_per_month = reward_per_sec * DURATION_30_DAYS_IN_SEC - let ideal_amount = self.monthly_reward * ((cur_timestamp_in_sec - self.prev_distribution_time_in_sec) / DURATION_30DAYS_IN_SEC) as u128; + 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 @@ -215,5 +214,101 @@ impl FungibleTokenReceiver for Contract { 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 From c43d8d6a493f1f6a6cc004423e96f499c52b6c50 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Thu, 21 Jul 2022 01:43:02 +0400 Subject: [PATCH 11/12] p3-nft --- Cargo.toml | 1 + p3-farm-nft/Cargo.toml | 16 + p3-farm-nft/Makefile | 15 + p3-farm-nft/P3_nft_explanation.md | 74 ++ p3-farm-nft/README.md | 79 ++ p3-farm-nft/TODO.org | 8 + p3-farm-nft/callbacks.md | 37 + p3-farm-nft/changes.txt | 5 + p3-farm-nft/src/constants.rs | 36 + p3-farm-nft/src/errors.rs | 8 + p3-farm-nft/src/helpers.rs | 80 ++ p3-farm-nft/src/interfaces.rs | 106 ++ p3-farm-nft/src/lib.rs | 1581 +++++++++++++++++++++++++ p3-farm-nft/src/storage_management.rs | 89 ++ p3-farm-nft/src/token_standards.rs | 140 +++ p3-farm-nft/src/vault.rs | 211 ++++ p3-farm/Cargo.toml | 1 + 17 files changed, 2487 insertions(+) create mode 100644 p3-farm-nft/Cargo.toml create mode 100644 p3-farm-nft/Makefile create mode 100644 p3-farm-nft/P3_nft_explanation.md create mode 100644 p3-farm-nft/README.md create mode 100644 p3-farm-nft/TODO.org create mode 100644 p3-farm-nft/callbacks.md create mode 100644 p3-farm-nft/changes.txt create mode 100644 p3-farm-nft/src/constants.rs create mode 100644 p3-farm-nft/src/errors.rs create mode 100644 p3-farm-nft/src/helpers.rs create mode 100644 p3-farm-nft/src/interfaces.rs create mode 100644 p3-farm-nft/src/lib.rs create mode 100644 p3-farm-nft/src/storage_management.rs create mode 100644 p3-farm-nft/src/token_standards.rs create mode 100644 p3-farm-nft/src/vault.rs diff --git a/Cargo.toml b/Cargo.toml index de5ea1d..6425f24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ # "./p1-staking-pool-dyn", "./p2-token-staking-fixed", "./p3-farm", + "./p3-farm-nft", "./xcheddar", "./p4-pool" ] 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..79f0d9f --- /dev/null +++ b/p3-farm-nft/README.md @@ -0,0 +1,79 @@ +# P3 Token Farm with Many Staked and Many Farmed token types. + +The P2-fixed farm allows to stake tokens and farm Cheddar. Constraints: + +- The total supply of farmed tokens is fixed = `total_harvested`. This is computed by `reward_rate * number_rounds`. +- Cheddar is farmed per round. During each round we farm `total_cheddar/number_rounds`. +- Each user, in each round will farm proportionally to the amount of 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/p3-farm/Cargo.toml b/p3-farm/Cargo.toml index 4e570d0..e8c7d42 100644 --- a/p3-farm/Cargo.toml +++ b/p3-farm/Cargo.toml @@ -9,6 +9,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] + serde = { version = "*", features = ["derive"] } serde_json = "*" uint = { version = "0.9.0", default-features = false } From 1584f72157da6c81dc6c6fdf325a4f6dd219a3c8 Mon Sep 17 00:00:00 2001 From: YellingOilbird <@guacharo.w3@yahoo.com> Date: Thu, 21 Jul 2022 01:45:01 +0400 Subject: [PATCH 12/12] Update README.md --- p3-farm-nft/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/p3-farm-nft/README.md b/p3-farm-nft/README.md index 79f0d9f..38eb996 100644 --- a/p3-farm-nft/README.md +++ b/p3-farm-nft/README.md @@ -1,10 +1,10 @@ -# P3 Token Farm with Many Staked and Many Farmed token types. +# P3 NFT Token Farm with Many Staked and Many Farmed token types. -The P2-fixed farm allows to stake tokens and farm Cheddar. Constraints: +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 is farmed per round. During each round we farm `total_cheddar/number_rounds`. -- Each user, in each round will farm proportionally to the amount of tokens (s)he staked. +- 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.