diff --git a/.gitignore b/.gitignore index 33f1912..d59e1c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target/** Cargo.lock +neardev/** +.env \ No newline at end of file diff --git a/fluxus-safe/.env.example b/fluxus-safe/.env.example index 16f806a..7cc991b 100644 --- a/fluxus-safe/.env.example +++ b/fluxus-safe/.env.example @@ -16,3 +16,4 @@ export token_id=":$pool_id" export seed_min_deposit="1000000000000000000" export croncat_manager="manager_v1.croncat.testnet" export total_gas=300000000000000 +export treasure_contract_id=dev-1656420526638-61041719201929 diff --git a/fluxus-safe/Cargo.toml b/fluxus-safe/Cargo.toml index 502b481..0e99c15 100644 --- a/fluxus-safe/Cargo.toml +++ b/fluxus-safe/Cargo.toml @@ -26,6 +26,7 @@ borsh = "0.9" maplit = "1.0" near-units = "0.1.0" # arbitrary_precision enabled for u128 types that workspaces requires for Balance types +serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0", features = ["arbitrary_precision"] } tracing = "0.1" tracing-subscriber = { version = "0.3.5", features = ["env-filter"] } diff --git a/fluxus-safe/Makefile b/fluxus-safe/Makefile index 325dc55..ce74190 100644 --- a/fluxus-safe/Makefile +++ b/fluxus-safe/Makefile @@ -1,5 +1,10 @@ +# e2e test test: export RUST_BACKTRACE=full ./build.sh cargo test --test '*' -- --nocapture +unit_test: + export RUST_BACKTRACE=full + ./build.sh + cargo test --lib -- --nocapture \ No newline at end of file diff --git a/fluxus-safe/build.sh b/fluxus-safe/build.sh index 9721a17..217a75d 100755 --- a/fluxus-safe/build.sh +++ b/fluxus-safe/build.sh @@ -9,5 +9,5 @@ fi RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release -cp target/wasm32-unknown-unknown/release/auto_compounder.wasm res/ +cp ../target/wasm32-unknown-unknown/release/fluxus_safe.wasm res/ diff --git a/fluxus-safe/res/auto_compounder.wasm b/fluxus-safe/res/auto_compounder.wasm deleted file mode 100755 index 2185849..0000000 Binary files a/fluxus-safe/res/auto_compounder.wasm and /dev/null differ diff --git a/fluxus-safe/res/fluxus_safe.wasm b/fluxus-safe/res/fluxus_safe.wasm new file mode 100644 index 0000000..69d58e8 Binary files /dev/null and b/fluxus-safe/res/fluxus_safe.wasm differ diff --git a/fluxus-safe/scripts/add_strategy.sh b/fluxus-safe/scripts/add_strategy.sh index 4863bf0..b980ce5 100755 --- a/fluxus-safe/scripts/add_strategy.sh +++ b/fluxus-safe/scripts/add_strategy.sh @@ -8,18 +8,30 @@ echo $reward_token #### Create first strategy near call $CONTRACT_NAME create_strategy '{ "_strategy": "", - "protocol_fee": 10, + "strategy_fee": 5, + "strat_creator": { "account_id": "'$username'", "fee_percentage": 5, "current_amount" : 0 }, + "sentry_fee": 10, "token1_address": "'$token1_address'", "token2_address": "'$token2_address'", - "pool_id_token1_reward": '$pool_id_token1_reward', - "pool_id_token2_reward": '$pool_id_token2_reward', - "reward_token": "'$reward_token'", - "farm": "'$farm_id'", "pool_id": '$pool_id', "seed_min_deposit": "1000000000000000000" }' --accountId $CONTRACT_NAME --gas $total_gas +near call $CONTRACT_NAME add_farm_to_strategy '{ + "pool_id": '$pool_id', + "pool_id_token1_reward": '$pool_id_token1_reward', + "pool_id_token2_reward": '$pool_id_token2_reward', + "reward_token": "'$reward_token'", + "farm_id": "'$farm_id'" +}' --accountId $CONTRACT_NAME --gas $total_gas # Register the contract in the pool #### TODO: move this call to create_auto_compounder method near call $exchange_contract_id mft_register '{ "token_id" : ":'$pool_id'", "account_id": "'$CONTRACT_NAME'" }' --accountId $CONTRACT_NAME --deposit 1 + +#At reward token +near call $reward_token storage_deposit '{"account_id": "'$CONTRACT_NAME'", "registration_only": false}' --accountId $CONTRACT_NAME --gas 300000000000000 --deposit 0.00125 + +# Register reward_token in the exchange in the contracts account whitelisted tokens +# only necessary for tokens that arent registered in the exchange already +near call $exchange_contract_id register_tokens '{ "token_ids" : [ "'$reward_token'" ] }' --accountId $CONTRACT_NAME --gas 300000000000000 --depositYocto 1 diff --git a/fluxus-safe/scripts/auto_compound.sh b/fluxus-safe/scripts/auto_compound.sh index b39ab98..5495d5d 100755 --- a/fluxus-safe/scripts/auto_compound.sh +++ b/fluxus-safe/scripts/auto_compound.sh @@ -6,10 +6,7 @@ source .env #### Functions managed by auto-compound -# near call $CONTRACT_NAME claim_reward '{"token_id": "'$token_id'"}' --accountId $CONTRACT_NAME --gas $total_gas - -# near call $CONTRACT_NAME withdraw_of_reward '{"token_id": "'$token_id'"}' --accountId $CONTRACT_NAME --gas $total_gas - -# near call $CONTRACT_NAME autocompounds_swap '{"token_id": "'$token_id'"}' --accountId $CONTRACT_NAME --gas $total_gas - -# near call $CONTRACT_NAME autocompounds_liquidity_and_stake '{"token_id": "'$token_id'"}' --accountId $CONTRACT_NAME --gas $total_gas +near call $CONTRACT_NAME harvest '{"farm_id_str": "'$farm_id_str'"}' --accountId $username --gas $total_gas +near call $CONTRACT_NAME harvest '{"farm_id_str": "'$farm_id_str'"}' --accountId $username --gas $total_gas +near call $CONTRACT_NAME harvest '{"farm_id_str": "'$farm_id_str'"}' --accountId $username --gas $total_gas +near call $CONTRACT_NAME harvest '{"farm_id_str": "'$farm_id_str'"}' --accountId $username --gas $total_gas \ No newline at end of file diff --git a/fluxus-safe/scripts/build.sh b/fluxus-safe/scripts/build.sh index 0fe12da..ebd2c87 100755 --- a/fluxus-safe/scripts/build.sh +++ b/fluxus-safe/scripts/build.sh @@ -9,5 +9,4 @@ fi RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release -cp ../target/wasm32-unknown-unknown/release/auto_compounder.wasm ../res/ - +cp ../../target/wasm32-unknown-unknown/release/fluxus_safe.wasm ../res/ diff --git a/fluxus-safe/scripts/deploy.sh b/fluxus-safe/scripts/deploy.sh new file mode 100755 index 0000000..d846065 --- /dev/null +++ b/fluxus-safe/scripts/deploy.sh @@ -0,0 +1,5 @@ +# build wasm +./build.sh + +#Deploy with near-dev +near dev-deploy --wasmFile ../res/fluxus_safe.wasm \ No newline at end of file diff --git a/fluxus-safe/scripts/initialize.sh b/fluxus-safe/scripts/initialize.sh index 7294074..046fe53 100755 --- a/fluxus-safe/scripts/initialize.sh +++ b/fluxus-safe/scripts/initialize.sh @@ -5,7 +5,6 @@ source .env echo $username echo $reward_token - #### Initialize contract near call $CONTRACT_NAME new '{ "owner_id":"'$username'", "exchange_contract_id": "'$exchange_contract_id'", "farm_contract_id": "'$farm_contract_id'", "treasure_contract_id": "'$treasure_contract_id'" }' --accountId $CONTRACT_NAME @@ -18,10 +17,4 @@ near call $CONTRACT_NAME call_user_register '{"account_id": "'$CONTRACT_NAME'"}' #At the farm near call $farm_contract_id storage_deposit '{"account_id": "'$CONTRACT_NAME'", "registration_only": false}' --accountId $CONTRACT_NAME --deposit 0.1 -#At reward token -near call $reward_token storage_deposit '{"account_id": "'$CONTRACT_NAME'", "registration_only": false}' --accountId $CONTRACT_NAME --gas 300000000000000 --deposit 0.00125 - -# Register reward_token in the exchange in the contracts account whitelisted tokens -# only necessary for tokens that arent registered in the exchange already -near call $exchange_contract_id register_tokens '{ "token_ids" : [ "'$reward_token'" ] }' --accountId $CONTRACT_NAME --gas 300000000000000 --depositYocto 1 diff --git a/fluxus-safe/scripts/mft_test.sh b/fluxus-safe/scripts/mft_test.sh new file mode 100755 index 0000000..4fcd55d --- /dev/null +++ b/fluxus-safe/scripts/mft_test.sh @@ -0,0 +1,10 @@ +source .env + +source neardev/dev-account.env +echo $CONTRACT_NAME + + +near call $CONTRACT_NAME register_seed '{"fft_share" : "seed1"}' --accountId $CONTRACT_NAME +near call $CONTRACT_NAME mft_mint '{"fft_share" : "seed1", "balance":100, "user": "'$username'"}' --accountId $CONTRACT_NAME +near call $CONTRACT_NAME mft_burn '{"fft_share" : "seed1", "balance":1, "user": '$username'}' --accountId $CONTRACT_NAME +near call $CONTRACT_NAME users_fft_share_amount '{"fft_share" : "seed1", "user": '$username'}' --accountId $CONTRACT_NAME diff --git a/fluxus-safe/scripts/run.sh b/fluxus-safe/scripts/run.sh index 5d263de..c9e3b0f 100755 --- a/fluxus-safe/scripts/run.sh +++ b/fluxus-safe/scripts/run.sh @@ -4,7 +4,7 @@ ./build.sh #Deploy with near-dev -near dev-deploy --wasmFile ../res/auto_compounder.wasm +near dev-deploy --wasmFile ../res/fluxus_safe.wasm source neardev/dev-account.env echo $CONTRACT_NAME @@ -16,13 +16,13 @@ echo $username #### initializes the contract, create strategy and registers in the necessary contracts ./initialize.sh -#### create strategy from .env +# #### create strategy from .env ./add_strategy.sh -#### storage_deposit + wrap_near + stake +# #### storage_deposit + wrap_near + stake ./stake_process.sh #### unstake_and_remove_liquidity + withdraw_to_contract # ./unstake_process.sh - \ No newline at end of file + diff --git a/fluxus-safe/scripts/stake_process.sh b/fluxus-safe/scripts/stake_process.sh index 85232e6..9e158ad 100755 --- a/fluxus-safe/scripts/stake_process.sh +++ b/fluxus-safe/scripts/stake_process.sh @@ -14,7 +14,10 @@ echo $username near call $exchange_contract_id mft_transfer_call '{"token_id": ":'$pool_id'", "receiver_id": "'$CONTRACT_NAME'", "amount": "1000000000000000000", "msg": "" }' --accountId $username --gas $total_gas --depositYocto 1 # ### Should have the previous amount plus the user shares -near view $farm_contract_id list_user_seeds '{ "account_id": "'$CONTRACT_NAME'" }' +# old farm contract +# near view $farm_contract_id list_user_seeds '{ "account_id": "'$CONTRACT_NAME'" }' +# boost farm contract +near view $farm_contract_id list_farmer_seeds '{ "farmer_id": "'$CONTRACT_NAME'" }' # ### Should be the same amount as passed in mft_transfer_call -near view $CONTRACT_NAME get_user_shares '{ "account_id": "'$username'", "token_id": "'$token_id'" }' +near view $CONTRACT_NAME user_share_seed_id '{ "seed_id": "'$seed_id'", "user": "'$username'" }' diff --git a/fluxus-safe/scripts/unstake_process.sh b/fluxus-safe/scripts/unstake_process.sh index cd92b9d..075b921 100755 --- a/fluxus-safe/scripts/unstake_process.sh +++ b/fluxus-safe/scripts/unstake_process.sh @@ -5,22 +5,22 @@ source .env echo $username # #### User shares on auto-compouder contract -near view $CONTRACT_NAME get_user_shares '{ "account_id": "'$username'", "token_id": "'$token_id'" }' +near view $CONTRACT_NAME user_share_seed_id '{ "seed_id": "'$seed_id'", "user": "'$username'" }' ### Auto-compoter staked shares near view $farm_contract_id list_user_seeds '{ "account_id": "'$CONTRACT_NAME'" }' #### Unstake the total amount available -near call $CONTRACT_NAME unstake '{ "token_id": ":'$pool_id'" }' --accountId $username --gas 300000000000000 +# near call $CONTRACT_NAME unstake '{ "token_id": ":'$pool_id'" }' --accountId $username --gas 300000000000000 #### Unstake given amount from contract -# near call $CONTRACT_NAME unstake '{ "token_id": ":'$pool_id'", "amount_withdrawal": "1005611400372449400" }' --accountId $username --gas 300000000000000 +near call $CONTRACT_NAME unstake '{ "token_id": ":'$pool_id'", "amount_withdrawal": "500000000000000000" }' --accountId $username --gas 300000000000000 #### Shoud have the contract shares minus the user shares near view $farm_contract_id list_user_seeds '{ "account_id": "'$CONTRACT_NAME'" }' ### Should be 0 after successful unstake -near view $CONTRACT_NAME get_user_shares '{ "account_id": "'$username'", "token_id": "'$token_id'" }' +near view $CONTRACT_NAME user_share_seed_id '{ "seed_id": "'$seed_id'", "user": "'$username'" }' ### Should have the previous shares on the auto-compounder contract near view $exchange_contract_id get_pool_shares '{ "pool_id": '$pool_id', "account_id" : "'$username'" }' diff --git a/fluxus-safe/scripts/views.sh b/fluxus-safe/scripts/views.sh index 0809fda..3e137ba 100755 --- a/fluxus-safe/scripts/views.sh +++ b/fluxus-safe/scripts/views.sh @@ -5,39 +5,114 @@ source .env source neardev/dev-account.env echo $CONTRACT_NAME -######## Farm contract -#### Get farm state, Running, Ended, etc. +######## Old farm contract + +#### Get all farms from given seed +# near view $farm_contract_id list_farms_by_seed '{ "seed_id": "'$exchange_contract_id'@'$pool_id'" }' + +### Get farm state, Running, Ended, etc. # near view $farm_contract_id get_farm '{ "farm_id": "'$exchange_contract_id'@'$pool_id'#'$farm_id'" }' -#### Get min deposit for seed -# near view $farm_contract_id get_seed_info '{ "seed_id": "'$exchange_contract_id'@'$pool_id'" }' +### Get min deposit for seed +# near view $farm_contract_id list_seeds_info '{ "seed_id": "'$exchange_contract_id'@'$pool_id'" }' #### Get unclaimed reward # near view $farm_contract_id get_unclaimed_reward '{ "account_id": "'$CONTRACT_NAME'", "farm_id": "'$exchange_contract_id'@'$pool_id'#'$farm_id'" }' +######## Boost farm contract +# near view $farm_contract_id list_seed_farms '{ "seed_id": "'$exchange_contract_id'@'$pool_id'" }' +# near view $farm_contract_id list_farmer_seeds '{ "farmer_id": "'$CONTRACT_NAME'" }' +# near view $farm_contract_id list_farmer_rewards '{ "farmer_id": "'$CONTRACT_NAME'" }' +# near view $farm_contract_id get_unclaimed_rewards '{ "farmer_id": "'$CONTRACT_NAME'", "seed_id": "'$seed_id'" }' +# near view $farm_contract_id list_seeds_info '{ "from_index": 0, "limit": 300 }' + ######## Safe contract #### Get the current contract state near view $CONTRACT_NAME get_contract_state +# 'contract_id is Running' #### Get exchange and farm contracts near view $CONTRACT_NAME get_contract_info +# { +# exchange_address: 'ref-finance-101.testnet', +# farm_address: 'boostfarm.ref-finance.testnet' +# } -#### Get tokens_id, :10, for strategies that are running +### Get tokens_id, :10, for strategies that are running near view $CONTRACT_NAME get_allowed_tokens '{}' +# [ ':50' ] #### Get all strats infos, such as state, token_id, reward_token, min_deposit -near view $CONTRACT_NAME get_strats '{}' +near view $CONTRACT_NAME get_strategies '{}' +# [ +# { +# token_id: ':50', +# is_active: true, +# reward_tokens: [ 'skyward.fakes.testnet' ] +# } +# ] #### Get state from strat, if its Running, Ended, etc. -near view $CONTRACT_NAME get_strat_state '{"token_id": "'$token_id'" }' +near view $CONTRACT_NAME get_strat_state '{"farm_id_str": "'$farm_id_str'" }' +# 'Running' -#### Returns struct from user {deposited: x, total: y} -near view $CONTRACT_NAME get_user_shares '{ "account_id": "'$username'", "token_id": ":'$pool_id'" }' +#### Returns number of shares the user has for given seed_id +near view $CONTRACT_NAME user_share_seed_id '{ "seed_id": "'$seed_id'", "user": "'$username'" }' +# 1000000000000000000 #### Get guardians near view $CONTRACT_NAME get_guardians '{}' +# [] + +#### Get total amount staked on contract +near view $CONTRACT_NAME get_contract_amount '{}' +# '0' + +#### Returns the total number of strategies/farms in this contract +near view $CONTRACT_NAME number_of_strategies '{}' +# 1 + +#### Returns the total staked for given seed +near view $CONTRACT_NAME seed_total_amount '{ "token_id": "'$token_id'" }' +# 1000000000000000000 + +#### Get fee percentage +near view $CONTRACT_NAME check_fee_by_strategy '{ "token_id": "'$token_id'" }' +# '5%' + +#### Returns true/false for given strategy +near view $CONTRACT_NAME is_strategy_active '{ "token_id": "'$token_id'" }' + +#### Returns strat step, ['claim_reward, 'withdraw', 'swap', 'stake'] +near view $CONTRACT_NAME current_strat_step '{ "farm_id_str": "'$farm_id_str'" }' +# 'claim_reward' + +#### Returns farm ids from given token_id +near view $CONTRACT_NAME get_farm_ids_by_seed '{ "token_id": "'$token_id'" }' +# [ ':50#1' ] + +#### Returns the timestamp of the last harvest for given token_id, '0' if it never occurred +near view $CONTRACT_NAME get_harvest_timestamp '{ "token_id": "'$token_id'" }' +# 1659470914303 + +#### Returns all infos for all strategies +near view $CONTRACT_NAME get_strategies_info '{}' +# [ +# { +# state: 'Running', +# cycle_stage: 'ClaimReward', +# slippage: 99, +# last_reward_amount: 0, +# last_fee_amount: 0, +# pool_id_token1_reward: 9999, +# pool_id_token2_reward: 50, +# reward_token: 'skyward.fakes.testnet', +# available_balance: [ 0, 0 ], +# id: '1' +# } +# ] -# #### Get total amount staked on contract -near view $CONTRACT_NAME get_contract_amount '{}' \ No newline at end of file +near view $CONTRACT_NAME get_strategy_kind '{}' +# 'AUTO_COMPOUNDER' \ No newline at end of file diff --git a/fluxus-safe/src/account_deposit.rs b/fluxus-safe/src/account_deposit.rs index 6fce42e..1a0695c 100644 --- a/fluxus-safe/src/account_deposit.rs +++ b/fluxus-safe/src/account_deposit.rs @@ -362,24 +362,24 @@ impl Contract { self.internal_save_account(&account_id, account); } - pub(crate) fn internal_register_account_sub( - &mut self, - account_id: &AccountId, - amount: Balance, - ) { - let mut account = self.internal_unwrap_or_default_account(&account_id); - log!( - "account.near_amount is = {} and amount = {}", - account.near_amount, - amount - ); - account.near_amount -= amount; - log!( - "the new balance after subtracting = {}", - account.near_amount - ); - self.internal_save_account(&account_id, account); - } + // pub(crate) fn internal_register_account_sub( + // &mut self, + // account_id: &AccountId, + // amount: Balance, + // ) { + // let mut account = self.internal_unwrap_or_default_account(&account_id); + // log!( + // "account.near_amount is = {} and amount = {}", + // account.near_amount, + // amount + // ); + // account.near_amount -= amount; + // log!( + // "the new balance after subtracting = {}", + // account.near_amount + // ); + // self.internal_save_account(&account_id, account); + // } /// storage withdraw pub(crate) fn internal_storage_withdraw( @@ -571,15 +571,15 @@ mod tests { // assert no storage is available until near is added assert_eq!(account.storage_available(), 0u128); account.deposit_with_storage_check(&to_account_id("uxu.near"), 10u128); - let uxu_balance: Option = account.get_balance(&to_account_id("uxu.near")); - assert_eq!(uxu_balance.unwrap_or(0u128), 0u128); + let fft_balance: Option = account.get_balance(&to_account_id("uxu.near")); + assert_eq!(fft_balance.unwrap_or(0u128), 0u128); account.near_amount = 100000000000000000000000u128; assert_ne!(account.storage_available(), 0u128); // deposit token with balance account.deposit_with_storage_check(&to_account_id("uxu.near"), 10u128); - let uxu_balance: Option = account.get_balance(&to_account_id("uxu.near")); - assert_eq!(uxu_balance.unwrap_or(1u128), 10u128); + let fft_balance: Option = account.get_balance(&to_account_id("uxu.near")); + assert_eq!(fft_balance.unwrap_or(1u128), 10u128); } } diff --git a/fluxus-safe/src/actions_of_compounder.rs b/fluxus-safe/src/actions_of_compounder.rs index addde1f..56013b1 100644 --- a/fluxus-safe/src/actions_of_compounder.rs +++ b/fluxus-safe/src/actions_of_compounder.rs @@ -10,7 +10,7 @@ impl Contract { self.data().farm_contract_id.clone(), token_id.clone(), U128(shares), - "".to_string(), + "\"Free\"".to_string(), self.data().exchange_contract_id.clone(), 1, Gas(80_000_000_000_000), @@ -29,31 +29,85 @@ impl Contract { #[private] pub fn callback_stake_result( &mut self, + #[callback_result] transfer_result: Result, token_id: String, account_id: AccountId, shares: u128, ) -> String { - // TODO: remove generic promise check - assert!(self.check_promise(), "ERR_STAKE_FAILED"); + if let Ok(amount) = transfer_result { + assert_eq!(amount.0, 0, "ERR_STAKE_FAILED"); + } else { + panic!("ERR_STAKE_FAILED"); + } - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect("ERR_TOKEN_ID_DOES_NOT_EXIST"); + let mut id = token_id; + id.remove(0).to_string(); - let compounder = strat.get_mut(); + //Total fft_share + let total_fft = self.total_supply_by_pool_id(id.clone()); + log!("total fft is = {}", total_fft); + let fft_share_id = self.convert_pool_id_in_fft_share(id.clone()); - // increment total shares deposited by account - compounder.increment_user_shares(&account_id, shares); + let data = self.data_mut(); + let seed_id: String = format!("{}@{}", data.exchange_contract_id, id); + + //Total seed_id + let total_seed = data.seed_id_amount.get(&seed_id).unwrap_or_default(); + + self.data_mut() + .seed_id_amount + .insert(&seed_id, &(total_seed + shares)); + + let fft_share_amount; + if total_fft == 0 { + fft_share_amount = shares; + } else { + fft_share_amount = + (U256::from(shares) * U256::from(total_fft) / U256::from(total_seed)).as_u128(); + } + + log!( + "{} {} will be minted for {}", + fft_share_amount, + fft_share_id, + account_id.to_string() + ); + self.mft_mint(fft_share_id, fft_share_amount, account_id.to_string()); - format!("The {} added {} to {}", account_id, shares, token_id) + format!( + "The {} added {} to {}", + account_id, fft_share_amount, seed_id + ) } /// Withdraw user lps and send it to the contract. - pub fn unstake(&self, token_id: String, amount_withdrawal: Option) -> Promise { + pub fn unstake(&mut self, token_id: String, amount_withdrawal: Option) -> Promise { let (caller_id, contract_id) = self.get_predecessor_and_current_account(); + let mut id = token_id.clone(); + id.remove(0).to_string(); + + let seed_id: String = format!("{}@{}", self.data_mut().exchange_contract_id, id); + + let fft_share_id = self.convert_pool_id_in_fft_share(id); + let mut user_fft_shares = + self.users_fft_share_amount(fft_share_id.clone(), caller_id.to_string()); + + //Total fft_share + let total_fft = self.total_supply_amount(fft_share_id); + + //Total seed_id + let total_seed = self + .data_mut() + .seed_id_amount + .get(&seed_id) + .unwrap_or_default(); + + //Converting user total fft_shares in seed_id: + let user_shares = (U256::from(user_fft_shares) * U256::from(total_seed) + / U256::from(total_fft)) + .as_u128(); + let strat = self .data() .strategies @@ -62,23 +116,21 @@ impl Contract { let compounder = strat.clone().get(); - let user_shares = compounder - .user_shares - .get(&caller_id) - .expect("ERR_ACCOUNT_DOES_NOT_EXIST"); - - assert!( - user_shares.total != 0, - "User does not have enough lps to withdraw" - ); - - let amount: U128 = amount_withdrawal.unwrap_or(U128(user_shares.total)); + let amount: U128; + if let Some(amount_withdrawal) = amount_withdrawal { + amount = amount_withdrawal; + user_fft_shares = (U256::from(amount_withdrawal.0) * U256::from(total_fft) + / U256::from(total_seed)) + .as_u128(); + } else { + amount = U128(user_shares); + } assert!( - user_shares.total >= amount.0, + user_shares >= amount.0, "{} is trying to withdrawal {} and only has {}", - caller_id.clone(), + caller_id, amount.0, - user_shares.total + user_shares ); log!("{} is trying to withdrawal {}", caller_id, amount.0); @@ -103,6 +155,7 @@ impl Contract { token_id, caller_id, amount.0, + user_fft_shares, contract_id, 0, Gas(20_000_000_000_000), @@ -143,14 +196,15 @@ impl Contract { let amount = withdraw_amount - shares_on_exchange; // withdraw missing amount from farm - ext_farm::withdraw_seed( + ext_farm::unlock_and_withdraw_seed( compounder.seed_id, + U128(0), U128(amount), - "".to_string(), self.data().farm_contract_id.clone(), 1, Gas(180_000_000_000_000), ) + // TODO: add callback and then call mft_transfer // transfer the total amount required .then(ext_exchange::mft_transfer( token_id.clone(), @@ -170,21 +224,27 @@ impl Contract { token_id: String, account_id: AccountId, amount: Balance, + fft_shares: Balance, ) { // TODO: remove generic promise check assert!(self.check_promise()); // assert!(mft_transfer_result.is_ok()); - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect("ERR_TOKEN_ID_DOES_NOT_EXIST"); - - let compounder = strat.get_mut(); - - // Decrement user shares - compounder.decrement_user_shares(&account_id, amount); + let mut id = token_id; + let data = self.data_mut(); + id.remove(0).to_string(); + let seed_id: String = format!("{}@{}", data.exchange_contract_id, id); + let total_seed = data.seed_id_amount.get(&seed_id).unwrap_or_default(); + self.data_mut() + .seed_id_amount + .insert(&seed_id, &(total_seed - amount)); + let fft_share_id = self + .data() + .fft_share_by_seed_id + .get(&seed_id) + .unwrap() + .clone(); + self.mft_burn(fft_share_id, fft_shares, account_id.to_string()); } } @@ -196,16 +256,15 @@ impl Contract { #[payable] pub fn call_swap( &mut self, - pool_id_to_swap: u64, + pool_id: u64, token_in: AccountId, token_out: AccountId, amount_in: Option, min_amount_out: U128, ) -> Promise { - assert!(self.check_promise(), "Previous tx failed."); ext_exchange::swap( vec![SwapAction { - pool_id: pool_id_to_swap, + pool_id, token_in, token_out, amount_in, @@ -294,6 +353,78 @@ impl Contract { Gas(180_000_000_000_000), ) } + + // /// Sentry user can redeem manually earned reward + // pub fn redeem_reward(&self, token_id: String) -> Promise { + // let sentry_acc_id = env::predecessor_account_id(); + + // let strat = self + // .data() + // .strategies + // .get(&token_id) + // .expect(ERR21_TOKEN_NOT_REG); + + // let compounder = strat.get_ref(); + + // assert!(compounder.admin_fees.sentries.contains_key(&sentry_acc_id)); + + // let amount = *compounder.admin_fees.sentries.get(&sentry_acc_id).unwrap(); + // ext_exchange::mft_transfer( + // compounder.reward_token.to_string(), + // sentry_acc_id.clone(), + // U128(amount), + // Some("".to_string()), + // self.data().exchange_contract_id.clone(), + // 1, + // Gas(20_000_000_000_000), + // ) + // .then(ext_self::callback_post_sentry_mft_transfer( + // token_id, + // sentry_acc_id, + // amount, + // env::current_account_id(), + // 0, + // Gas(20_000_000_000_000), + // )) + // } + + /// Returns the amount of unclaimed reward given token_id has + pub fn get_unclaimed_rewards(&self, farm_id_str: String) -> Promise { + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); + + ext_farm::get_unclaimed_rewards( + env::current_account_id(), + seed_id, + self.data().farm_contract_id.clone(), + 1, + Gas(3_000_000_000_000), + ) + .then(ext_self::callback_post_unclaimed_rewards( + farm_info.reward_token, + env::current_account_id(), + 0, + Gas(10_000_000_000_000), + )) + } + + #[private] + pub fn callback_post_unclaimed_rewards( + &self, + #[callback_result] rewards_result: Result, PromiseError>, + reward_token: AccountId, + ) -> U128 { + if let Ok(tokens) = rewards_result { + if tokens.contains_key(&reward_token.to_string()) { + return *tokens.get(&reward_token.to_string()).unwrap(); + } + } + + U128(0) + } } /// Auto-compounder functionality methods diff --git a/fluxus-safe/src/actions_of_strat.rs b/fluxus-safe/src/actions_of_strat.rs index c9da548..3bc15d8 100644 --- a/fluxus-safe/src/actions_of_strat.rs +++ b/fluxus-safe/src/actions_of_strat.rs @@ -6,38 +6,130 @@ impl Contract { pub fn create_strategy( &mut self, _strategy: String, - protocol_fee: u128, + strategy_fee: u128, + strat_creator: AccountFee, + sentry_fee: u128, token1_address: AccountId, token2_address: AccountId, - pool_id_token1_reward: u64, - pool_id_token2_reward: u64, - reward_token: AccountId, - farm: String, pool_id: u64, seed_min_deposit: U128, ) -> String { self.is_owner(); - let seed_id: String = format!("{}@{}", self.data().exchange_contract_id, pool_id); - let farm_id: String = format!("{}#{}", seed_id, farm); let token_id = self.wrap_mft_token_id(&pool_id.to_string()); - self.data_mut().token_ids.push(token_id.clone()); - let strat: VersionedStrategy = VersionedStrategy::AutoCompounder(AutoCompounder::new( - protocol_fee, - token1_address, - token2_address, + return if self.data().strategies.contains_key(&token_id) { + format!("VersionedStrategy for {} already exist", token_id) + } else { + let seed_id: String = format!("{}@{}", self.data().exchange_contract_id, pool_id); + let uxu_share_id = self.new_fft_share(seed_id.clone()); + + let data_mut = self.data_mut(); + let treasury = data_mut.treasury.clone(); + data_mut.token_ids.push(token_id.clone()); + + let strat: VersionedStrategy = VersionedStrategy::AutoCompounder(AutoCompounder::new( + strategy_fee, + treasury, + strat_creator, + sentry_fee, + token1_address, + token2_address, + pool_id, + seed_id.clone(), + seed_min_deposit, + )); + + if let Some(id) = uxu_share_id { + log!("Registering {} to {}", id, seed_id); + //Registering id for the specific seed + data_mut.fft_share_by_seed_id.insert(seed_id, id.clone()); + + //Registering id in the users balance map + let temp = LookupMap::new(StorageKey::Strategy); + data_mut + .users_balance_by_fft_share + .insert(&id.clone(), &temp); + + //Registering total_supply + data_mut.total_supply_by_fft_share.insert(&id, &0_u128); + } + + data_mut.strategies.insert(token_id.clone(), strat); + + format!("VersionedStrategy for {} created successfully", token_id) + }; + } + + pub fn add_farm_to_strategy( + &mut self, + pool_id: u64, + pool_id_token1_reward: u64, + pool_id_token2_reward: u64, + reward_token: AccountId, + farm_id: String, + ) -> String { + self.is_owner(); + let token_id = self.wrap_mft_token_id(&pool_id.to_string()); + + // let data_mut = self.data_mut(); + let compounder = self.get_strat_mut(&token_id).get_mut(); + + for farm in compounder.farms.clone() { + if farm.id == farm_id { + return format!("Farm with index {} for {} already exist", farm_id, token_id); + } + } + + let farm_info: StratFarmInfo = StratFarmInfo { + state: AutoCompounderState::Running, + cycle_stage: AutoCompounderCycle::ClaimReward, + slippage: 99u128, + last_reward_amount: 0u128, + last_fee_amount: 0u128, pool_id_token1_reward, pool_id_token2_reward, reward_token, - farm_id, - pool_id, - seed_id, - seed_min_deposit, - )); + available_balance: vec![0u128, 0u128], + id: farm_id.clone(), + }; + + compounder.farms.push(farm_info); - self.data_mut().strategies.insert(token_id.clone(), strat); + format!( + "Farm with index {} for {} created successfully", + farm_id, token_id + ) + } + + fn new_fft_share(&mut self, seed_id: String) -> Option { + let already_has = self.data_mut().fft_share_by_seed_id.get(&seed_id).is_some(); + let fft_share_id; + if already_has { + fft_share_id = None + } else { + let num: u128 = + u128::try_from(self.data_mut().fft_share_by_seed_id.keys().len()).unwrap() + 1_u128; + fft_share_id = Some(format!("fft_share_{num}")); + log!( + "new fft_share created: {} for seed_id {}", + fft_share_id.clone().unwrap(), + seed_id + ); + } - format!("VersionedStrategy for {} created successfully", token_id) + fft_share_id + } + pub fn harvest(&mut self, farm_id_str: String) -> Promise { + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); + match farm_info.cycle_stage { + AutoCompounderCycle::ClaimReward => self.claim_reward(farm_id_str), + AutoCompounderCycle::Withdrawal => self.withdraw_of_reward(farm_id_str), + AutoCompounderCycle::Swap => self.autocompounds_swap(farm_id_str), + AutoCompounderCycle::Stake => self.autocompounds_liquidity_and_stake(farm_id_str), + } } } diff --git a/fluxus-safe/src/admin_fee.rs b/fluxus-safe/src/admin_fee.rs new file mode 100644 index 0000000..0b856de --- /dev/null +++ b/fluxus-safe/src/admin_fee.rs @@ -0,0 +1,68 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{env, AccountId}; +use std::collections::HashMap; + +const MAX_STRAT_CREATOR_FEE: u128 = 20; + +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct AccountFee { + /// address id + pub account_id: AccountId, + /// fee percentage + pub fee_percentage: u128, + /// current amount earned, stored to be used if tx fails + pub current_amount: u128, +} + +impl AccountFee { + pub fn new(acc_id: AccountId, fee: u128) -> Self { + assert!( + (0..MAX_STRAT_CREATOR_FEE + 1).contains(&fee), + "ERR_FEE_NOT_VALID" + ); + + AccountFee { + account_id: acc_id, + fee_percentage: fee, + current_amount: 0u128, + } + } +} + +const MAX_CONTRIBUTOR_FEE: u128 = 20; +const MAX_PROTOCOL_FEE: u128 = 20; + +/// Maintain information about fees. +/// Maps receiver address to percentage +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct AdminFees { + /// Protocol Total fees of the running strategy + pub strategy_fee: u128, + /// Fees earned by the creator of the running strategy + pub strat_creator: AccountFee, + /// Fee percentage earned by sentries + pub sentries_fee: u128, + /// Fees earned by users that interact with the harvest method - TODO: is this really needed seems a bit of storage waste + pub sentries: HashMap, +} + +impl AdminFees { + pub fn new(strat_creator: AccountFee, sentries_fee: u128, strategy_fee: u128) -> Self { + assert!( + strat_creator.fee_percentage + sentries_fee + <= MAX_CONTRIBUTOR_FEE + ); + assert!( + strategy_fee <= MAX_PROTOCOL_FEE + ); + AdminFees { + strategy_fee, + strat_creator, + sentries_fee, + sentries: HashMap::new(), + } + } +} diff --git a/fluxus-safe/src/auto_compound.rs b/fluxus-safe/src/auto_compound.rs index af63d41..2687c46 100644 --- a/fluxus-safe/src/auto_compound.rs +++ b/fluxus-safe/src/auto_compound.rs @@ -2,70 +2,75 @@ use near_sdk::PromiseOrValue; use crate::*; +const MIN_SLIPPAGE_ALLOWED: u128 = 1; + #[near_bindgen] impl Contract { /// Step 1 /// Function to claim the reward from the farm contract - pub fn claim_reward(&mut self, token_id: String) -> Promise { - self.assert_strategy_running(token_id.clone()); - self.is_allowed_account(); + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + #[private] + pub fn claim_reward(&mut self, farm_id_str: String) -> Promise { + self.assert_strategy_not_cleared(&farm_id_str); + log!("claim_reward"); - let strat = self.get_strat(&token_id); - let seed_id: String = strat.get_ref().seed_id.clone(); - let farm_id: String = strat.get_ref().farm_id.clone(); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); - ext_farm::list_farms_by_seed( + ext_farm::list_seed_farms( seed_id, self.data().farm_contract_id.clone(), 0, Gas(40_000_000_000_000), ) .then(ext_self::callback_list_farms_by_seed( - token_id, - farm_id, + farm_id_str, env::current_account_id(), 0, - Gas(120_000_000_000_000), + Gas(100_000_000_000_000), )) } + /// Check if farm still have rewards to distribute (status == Running) + /// Args: + /// farm_id_str: exchange@pool_id#farm_id pub fn callback_list_farms_by_seed( &mut self, - #[callback_result] farms_result: Result, PromiseError>, - token_id: String, - farm_id: String, + #[callback_result] farms_result: Result, PromiseError>, + farm_id_str: String, ) -> PromiseOrValue { assert!(farms_result.is_ok(), "ERR_LIST_FARMS_FAILED"); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let farms = farms_result.unwrap(); + // Try to unclaim before change to Ended for farm in farms.iter() { - if farm.farm_id == farm_id && farm.farm_status != *"Running" { - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); - let compounder = strat.get_mut(); - - compounder.state = AutoCompounderState::Ended; - return PromiseOrValue::Value(format!("The farm {:#?} ended", farm_id)); + if farm.farm_id == farm_id && farm.status != *"Running" { + let compounder = self.get_strat_mut(&token_id.to_string()).get_mut(); + + for strat_farm in compounder.farms.iter_mut() { + if strat_farm.id == farm_id { + strat_farm.state = AutoCompounderState::Ended; + } + } } } PromiseOrValue::Promise( - ext_farm::get_unclaimed_reward( + ext_farm::get_unclaimed_rewards( env::current_account_id(), - farm_id, + seed_id, self.data().farm_contract_id.clone(), 1, Gas(3_000_000_000_000), ) .then(ext_self::callback_post_get_unclaimed_reward( - token_id, + farm_id_str, env::current_account_id(), 0, - Gas(50_000_000_000_000), + Gas(70_000_000_000_000), )), ) } @@ -73,110 +78,255 @@ impl Contract { #[private] pub fn callback_post_get_unclaimed_reward( &mut self, - #[callback_result] reward_amount_result: Result, - token_id: String, - ) -> Promise { + #[callback_result] reward_amount_result: Result, PromiseError>, + farm_id_str: String, + ) -> PromiseOrValue { assert!(reward_amount_result.is_ok(), "ERR_GET_REWARD_FAILED"); - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); + let mut rewards_map = reward_amount_result.unwrap(); + + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let strat = self.get_strat_mut(&token_id); - let reward_amount = reward_amount_result.unwrap(); let compounder = strat.get_mut(); - // store the amount of reward earned - compounder.last_reward_amount = reward_amount.0; + let farm_info = compounder.get_mut_farm_info(farm_id); - ext_farm::claim_reward_by_farm( - strat.get_ref().farm_id.clone(), - self.data().farm_contract_id.clone(), - 0, - Gas(40_000_000_000_000), + for (token, amount) in rewards_map.iter() { + log!("token: {} amount: {}", token, amount.0); + } + + // this should never panics from .unwrap(), given that the reward_token was previously known + let reward_amount: U128 = if rewards_map.contains_key(&farm_info.reward_token.to_string()) { + rewards_map + .remove(&farm_info.reward_token.to_string()) + .unwrap() + } else { + U128(0) + }; + + if reward_amount.0 == 0u128 { + // if farm is ended, there is no more actions to do + if farm_info.state == AutoCompounderState::Ended { + farm_info.state = AutoCompounderState::Cleared; + return PromiseOrValue::Value(0u128); + } else { + panic!("ERR: zero rewards earned") + } + } + + PromiseOrValue::Promise( + ext_farm::claim_reward_by_seed( + seed_id, + self.data().farm_contract_id.clone(), + 0, + Gas(40_000_000_000_000), + ) + .then(ext_self::callback_post_claim_reward( + farm_id_str, + reward_amount, + rewards_map, + env::current_account_id(), + 0, + Gas(10_000_000_000_000), + )), ) } - /// Step 2 - /// Function to claim the reward from the farm contract - pub fn withdraw_of_reward(&mut self, token_id: String) -> Promise { - self.assert_strategy_running(token_id.clone()); - self.is_allowed_account(); + #[private] + pub fn callback_post_claim_reward( + &mut self, + #[callback_result] claim_reward_result: Result<(), PromiseError>, + farm_id_str: String, + reward_amount: U128, + rewards_map: HashMap, + ) -> u128 { + assert!(claim_reward_result.is_ok(), "ERR_WITHDRAW_FAILED"); - let strat = self.get_strat(&token_id); - let reward_token: AccountId = strat.clone().get().reward_token; + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); - let compounder = strat.get_ref(); + // update strategies with the same seed + let compounder = self.get_strat_mut(&token_id.to_string()).get_mut(); + compounder.update_strats_by_seed(rewards_map); - let amount_to_withdraw = compounder.last_reward_amount; + // store the amount of reward earned + let farm_info = compounder.get_mut_farm_info(farm_id); + farm_info.last_reward_amount += reward_amount.0; - ext_farm::withdraw_reward( - reward_token.to_string(), - U128(amount_to_withdraw), - "false".to_string(), - self.data().farm_contract_id.clone(), - 1, - Gas(180_000_000_000_000), - ) - .then(ext_self::callback_post_withdraw( - token_id, - env::current_account_id(), - 0, - Gas(60_000_000_000_000), - )) + farm_info.next_cycle(); + + reward_amount.0 + } + + /// Step 2 + /// Function to claim the reward from the farm contract + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + #[private] + pub fn withdraw_of_reward(&mut self, farm_id_str: String) -> Promise { + self.assert_strategy_not_cleared(&farm_id_str); + log!("withdraw_of_reward"); + + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let strat = self.get_strat(token_id); + let compounder = strat.get(); + let farm_info = compounder.get_farm_info(&farm_id); + + // contract_id does not exist on sentries + if !compounder + .admin_fees + .sentries + .contains_key(&env::current_account_id()) + { + let amount_to_withdraw = farm_info.last_reward_amount; + ext_farm::withdraw_reward( + farm_info.reward_token.to_string(), + U128(amount_to_withdraw), + "false".to_string(), + self.data().farm_contract_id.clone(), + 0, + Gas(180_000_000_000_000), + ) + .then(ext_self::callback_post_withdraw( + farm_id_str, + env::current_account_id(), + 0, + Gas(80_000_000_000_000), + )) + } else { + // the withdraw succeeded but not the transfer + ext_reward_token::ft_transfer_call( + self.exchange_acc(), + U128(farm_info.last_reward_amount + self.data().treasury.current_amount), //Amount after withdraw the rewards + "".to_string(), + farm_info.reward_token, + 1, + Gas(40_000_000_000_000), + ) + .then(ext_self::callback_post_ft_transfer( + farm_id_str, + env::current_account_id(), + 0, + Gas(20_000_000_000_000), + )) + } } #[private] pub fn callback_post_withdraw( &mut self, - #[callback_result] withdraw_result: Result, - token_id: String, + #[callback_result] withdraw_result: Result, + farm_id_str: String, ) -> PromiseOrValue { + assert!(withdraw_result.is_ok(), "ERR_WITHDRAW_FROM_FARM_FAILED"); + + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let exchange_id = self.exchange_acc(); - let strat = self - .data_mut() + let data_mut = self.data_mut(); + + let strat = data_mut .strategies .get_mut(&token_id) .expect(ERR21_TOKEN_NOT_REG); let compounder = strat.get_mut(); - if withdraw_result.is_err() { - compounder.last_reward_amount = 0u128; - log!("ERR_WITHDRAW_FROM_FARM_FAILED"); - return PromiseOrValue::Value(U128(0)); - } + // let farm_info_mut = compounder.get_mut_farm_info(farm_id); - PromiseOrValue::Promise(ext_reward_token::ft_transfer_call( - exchange_id, - compounder.last_reward_amount.to_string(), //Amount after withdraw the rewards - "".to_string(), - compounder.reward_token.clone(), - 1, - Gas(40_000_000_000_000), - )) + let last_reward_amount = compounder + .get_mut_farm_info(farm_id.clone()) + .last_reward_amount; + + let (remaining_amount, protocol_amount, sentry_amount, strat_creator_amount) = + compounder.compute_fees(last_reward_amount); + + // storing the amount earned by the strat creator + compounder.admin_fees.strat_creator.current_amount += strat_creator_amount; + + // store sentry amount under contract account id to be used in the last step + compounder + .admin_fees + .sentries + .insert(env::current_account_id(), sentry_amount); + + // increase protocol amount to cover the case that the last transfer failed + data_mut.treasury.current_amount += protocol_amount; + + // remaining amount to reinvest + compounder + .get_mut_farm_info(farm_id.clone()) + .last_reward_amount = remaining_amount; + + // amount sent to ref, both remaining value and treasury + let amount = remaining_amount + protocol_amount; + + PromiseOrValue::Promise( + ext_reward_token::ft_transfer_call( + exchange_id, + U128(amount), //Amount after withdraw the rewards + "".to_string(), + compounder.get_mut_farm_info(farm_id).reward_token.clone(), + 1, + Gas(40_000_000_000_000), + ) + .then(ext_self::callback_post_ft_transfer( + farm_id_str, + env::current_account_id(), + 0, + Gas(20_000_000_000_000), + )), + ) } - /// Step 3 - /// Transfer lp tokens to ref-exchange then swap the amount the contract has in the exchange - pub fn autocompounds_swap(&mut self, token_id: String) -> Promise { - self.assert_strategy_running(token_id.clone()); - self.is_allowed_account(); + #[private] + pub fn callback_post_ft_transfer( + &mut self, + #[callback_result] exchange_transfer_result: Result, + farm_id_str: String, + ) { + if exchange_transfer_result.is_err() { + log!("ERR_TRANSFER_TO_EXCHANGE"); + return; + } - let treasure_id = self.treasure_acc(); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); - let strat = self - .data_mut() + let data_mut = self.data_mut(); + let strat = data_mut .strategies .get_mut(&token_id) .expect(ERR21_TOKEN_NOT_REG); + let compounder = strat.get_mut(); + let farm_info_mut = compounder.get_mut_farm_info(farm_id); + farm_info_mut.next_cycle(); + } + + /// Step 3 + /// Transfer reward token to ref-exchange then swap the amount the contract has in the exchange + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + #[private] + pub fn autocompounds_swap(&mut self, farm_id_str: String) -> Promise { + // TODO: take string as ref + self.assert_strategy_not_cleared(&farm_id_str); + log!("autocompounds_swap"); + + let treasury_acc: AccountId = self.treasure_acc(); + let treasury_curr_amount: u128 = self.data_mut().treasury.current_amount; + + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str.clone()); + let strat = self.get_strat(token_id.clone()); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); let token1 = compounder.token1_address.clone(); let token2 = compounder.token2_address.clone(); - let reward = compounder.reward_token.clone(); + let reward = farm_info.reward_token.clone(); let mut common_token = 0; @@ -186,111 +336,138 @@ impl Contract { common_token = 2; } - assert!( - compounder.last_reward_amount > 0, - "ERR_NO_REWARD_AVAILABLE_TO_SWAP" - ); + let reward_amount = farm_info.last_reward_amount; - // apply fee to reward amount - // increase last_fee_amount, to cover the case that the last transfer somehow failed - let percent = Percentage::from(compounder.protocol_fee); + // This works by increasing gradually the slippage allowed + // It will be used only in the cases where the first swaps succeed but not the second + if farm_info.available_balance[0] > 0 { + common_token = 1; - let fee_amount = percent.apply_to(compounder.last_reward_amount); - compounder.last_fee_amount += fee_amount; + return self + .get_tokens_return( + farm_id_str.clone(), + U128(farm_info.available_balance[0]), + U128(reward_amount), + common_token, + ) + .then(ext_self::swap_to_auto( + farm_id_str, + U128(farm_info.available_balance[0]), + U128(reward_amount), + common_token, + env::current_account_id(), + 0, + Gas(140_000_000_000_000), + )); + } - // remaining amount to reinvest - let reward_amount = compounder.last_reward_amount - fee_amount; + let amount_in = U128(reward_amount / 2); - // ensure that the value used is less than or equal to the current available amount - assert!( - reward_amount + fee_amount <= compounder.last_reward_amount, - "ERR" - ); + if treasury_curr_amount > 0 { + ext_exchange::mft_transfer( + farm_info.reward_token.to_string(), + treasury_acc, + U128(treasury_curr_amount), + Some("".to_string()), + self.data().exchange_contract_id.clone(), + 1, + Gas(20_000_000_000_000), + ) + .then(ext_self::callback_post_treasury_mft_transfer( + env::current_account_id(), + 0, + Gas(20_000_000_000_000), + )); + } - let amount_in = U128(reward_amount / 2); + let strat_creator_curr_amount = compounder.admin_fees.strat_creator.current_amount; + if strat_creator_curr_amount > 0 { + ext_reward_token::ft_transfer( + compounder.admin_fees.strat_creator.account_id.clone(), + U128(strat_creator_curr_amount), + Some("".to_string()), + farm_info.reward_token, + 1, + Gas(20_000_000_000_000), + ) + .then(ext_self::callback_post_creator_ft_transfer( + token_id, + env::current_account_id(), + 0, + Gas(10_000_000_000_000), + )); + } - ext_exchange::mft_transfer( - compounder.reward_token.to_string(), - treasure_id, - U128(compounder.last_fee_amount), - Some("".to_string()), - self.data().exchange_contract_id.clone(), - 1, - Gas(20_000_000_000_000), - ) - .then(ext_self::callback_post_mft_transfer( - token_id.clone(), - env::current_account_id(), - 0, - Gas(20_000_000_000_000), - )) - .then(ext_self::get_tokens_return( - token_id.clone(), - amount_in, - amount_in, - common_token, - env::current_account_id(), - 0, - Gas(60_000_000_000_000), - )) - .then(ext_self::swap_to_auto( - token_id, - amount_in, - amount_in, - common_token, - env::current_account_id(), - 0, - Gas(100_000_000_000_000), - )) + self.get_tokens_return(farm_id_str.clone(), amount_in, amount_in, common_token) + .then(ext_self::swap_to_auto( + farm_id_str, + amount_in, + amount_in, + common_token, + env::current_account_id(), + 0, + Gas(140_000_000_000_000), + )) } /// Callback to verify that transfer to treasure succeeded #[private] - pub fn callback_post_mft_transfer( + pub fn callback_post_treasury_mft_transfer( &mut self, #[callback_result] ft_transfer_result: Result<(), PromiseError>, - token_id: String, ) { - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); - - let compounder = strat.get_mut(); + let data_mut = self.data_mut(); // in the case where the transfer failed, the next cycle will send it plus the new amount earned if ft_transfer_result.is_err() { - log!("Transfer to treasure failed".to_string()); + log!("Transfer to treasure failed"); return; } - let amount = compounder.last_fee_amount; + let amount: u128 = data_mut.treasury.current_amount; - // ensures that no duplicated value is sent to treasure - compounder.last_fee_amount = 0; + // reset treasury amount earned since tx was successful + data_mut.treasury.current_amount = 0; log!("Transfer {} to treasure succeeded", amount) } + #[private] + pub fn callback_post_creator_ft_transfer( + &mut self, + #[callback_result] strat_creator_transfer_result: Result<(), PromiseError>, + token_id: String, + ) { + if strat_creator_transfer_result.is_err() { + log!("ERR_TRANSFER_TO_CREATOR"); + return; + } + + let strat = self.get_strat_mut(&token_id); + let compounder = strat.get_mut(); + + compounder.admin_fees.strat_creator.current_amount = 0; + log!("Transfer fees to the creator of the strategy succeeded"); + } + #[private] pub fn get_tokens_return( &self, - #[callback_result] ft_transfer_result: Result<(), PromiseError>, - token_id: String, + farm_id_str: String, amount_token_1: U128, amount_token_2: U128, common_token: u64, ) -> Promise { - assert!(ft_transfer_result.is_ok(), "ERR_REWARD_TRANSFER_FAILED"); - - let strat = self.get_strat(&token_id); + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str); + let strat = self.get_strat(token_id); let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); if common_token == 1 { + // TODO: can be shortened by call_get_return ext_exchange::get_return( - compounder.pool_id_token2_reward, - compounder.reward_token.clone(), + farm_info.pool_id_token2_reward, + farm_info.reward_token, amount_token_2, compounder.token2_address.clone(), self.data().exchange_contract_id.clone(), @@ -306,8 +483,8 @@ impl Contract { )) } else if common_token == 2 { ext_exchange::get_return( - compounder.pool_id_token1_reward, - compounder.reward_token.clone(), + farm_info.pool_id_token1_reward, + farm_info.reward_token, amount_token_1, compounder.token1_address.clone(), self.data().exchange_contract_id.clone(), @@ -323,8 +500,8 @@ impl Contract { )) } else { ext_exchange::get_return( - compounder.pool_id_token1_reward, - compounder.reward_token.clone(), + farm_info.pool_id_token1_reward, + farm_info.reward_token.clone(), amount_token_1, compounder.token1_address.clone(), self.data().exchange_contract_id.clone(), @@ -332,8 +509,8 @@ impl Contract { Gas(10_000_000_000_000), ) .and(ext_exchange::get_return( - compounder.pool_id_token2_reward, - compounder.reward_token.clone(), + farm_info.pool_id_token2_reward, + farm_info.reward_token, amount_token_2, compounder.token2_address.clone(), self.data().exchange_contract_id.clone(), @@ -359,8 +536,6 @@ impl Contract { let amount: U128 = token_out.unwrap(); - assert!(amount.0 > 0u128, "ERR_SLIPPAGE_TOO_HIGH"); - if common_token == 1 { (amount_token, amount) } else { @@ -380,9 +555,6 @@ impl Contract { let amount_token1: U128 = token1_out.unwrap(); let amount_token2: U128 = token2_out.unwrap(); - assert!(amount_token1.0 > 0u128, "ERR_SLIPPAGE_TOO_HIGH"); - assert!(amount_token2.0 > 0u128, "ERR_SLIPPAGE_TOO_HIGH"); - (amount_token1, amount_token2) } @@ -391,38 +563,30 @@ impl Contract { pub fn swap_to_auto( &mut self, #[callback_unwrap] tokens: (U128, U128), - token_id: String, + farm_id_str: String, amount_in_1: U128, amount_in_2: U128, common_token: u64, ) -> Promise { - let (_, contract_id) = self.get_predecessor_and_current_account(); - - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); - let compounder = strat.get_mut(); + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder_mut = self.get_strat_mut(&token_id).get_mut(); + let token_out1 = compounder_mut.token1_address.clone(); + let token_out2 = compounder_mut.token2_address.clone(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); - let pool_id_to_swap1 = compounder.pool_id_token1_reward; - let pool_id_to_swap2 = compounder.pool_id_token2_reward; - let token_in1 = compounder.reward_token.clone(); - let token_in2 = compounder.reward_token.clone(); - let token_out1 = compounder.token1_address.clone(); - let token_out2 = compounder.token2_address.clone(); + let pool_id_to_swap1 = farm_info_mut.pool_id_token1_reward; + let pool_id_to_swap2 = farm_info_mut.pool_id_token2_reward; + let token_in1 = farm_info_mut.reward_token.clone(); + let token_in2 = farm_info_mut.reward_token.clone(); let (mut token1_min_out, mut token2_min_out): (U128, U128) = tokens; // apply slippage - let percent = Percentage::from(compounder.slippage); + let percent = Percentage::from(farm_info_mut.slippage); token1_min_out = U128(percent.apply_to(token1_min_out.0)); token2_min_out = U128(percent.apply_to(token2_min_out.0)); - compounder.available_balance[0] = token1_min_out.0; - compounder.available_balance[1] = token2_min_out.0; - log!( "min amount out: {} for {} and {} for {}", token1_min_out.0, @@ -431,10 +595,9 @@ impl Contract { token_out2 ); - //Actualization of reward amount - compounder.last_reward_amount = 0; - if common_token == 1 { + // use the entire amount for the common token + farm_info_mut.available_balance[0] = amount_in_1.0; self.call_swap( pool_id_to_swap2, token_in2, @@ -442,7 +605,16 @@ impl Contract { Some(amount_in_2), token2_min_out, ) + .then(ext_self::callback_post_swap( + farm_id_str, + common_token, + env::current_account_id(), + 0, + Gas(80_000_000_000_000), + )) } else if common_token == 2 { + // use the entire amount for the common token + farm_info_mut.available_balance[1] = amount_in_2.0; self.call_swap( pool_id_to_swap1, token_in1, @@ -450,6 +622,13 @@ impl Contract { Some(amount_in_1), token1_min_out, ) + .then(ext_self::callback_post_swap( + farm_id_str, + common_token, + env::current_account_id(), + 0, + Gas(20_000_000_000_000), + )) } else { self.call_swap( pool_id_to_swap1, @@ -458,78 +637,311 @@ impl Contract { Some(amount_in_1), token1_min_out, ) - // TODO: should use and - .then(ext_self::call_swap( + .then(ext_self::callback_post_first_swap( + farm_id_str, + common_token, + amount_in_2, + token2_min_out, + env::current_account_id(), + 0, + Gas(80_000_000_000_000), + )) + } + } + + #[private] + pub fn callback_post_first_swap( + &mut self, + #[callback_result] swap_result: Result, + farm_id_str: String, + common_token: u64, + amount_in: U128, + token_min_out: U128, + ) -> PromiseOrValue { + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder_mut = self.get_strat_mut(&token_id).get_mut(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); + + // Do not panic if err == true, otherwise the slippage update will not be applied + if swap_result.is_err() { + farm_info_mut.increase_slippage(); + log!("ERR_FIRST_SWAP_FAILED"); + + return PromiseOrValue::Value(0u64); + } + + farm_info_mut.available_balance[0] = swap_result.unwrap().0; + + // First swap succeeded, thus decrement the last reward_amount + let amount_used: u128 = farm_info_mut.last_reward_amount / 2; + farm_info_mut.last_reward_amount -= amount_used; + + let pool_id_to_swap2 = farm_info_mut.pool_id_token2_reward; + let token_in2 = farm_info_mut.reward_token.clone(); + let token_out2 = compounder_mut.token2_address.clone(); + + PromiseOrValue::Promise( + ext_self::call_swap( pool_id_to_swap2, token_in2, token_out2, - Some(amount_in_2), - token2_min_out, - contract_id, + Some(amount_in), + token_min_out, + env::current_account_id(), 0, - Gas(40_000_000_000_000), - )) // should use a callback to assert that both tx succeeded + Gas(30_000_000_000_000), + ) + .then(ext_self::callback_post_swap( + farm_id_str, + common_token, + env::current_account_id(), + 0, + Gas(20_000_000_000_000), + )), + ) + } + + #[private] + pub fn callback_post_swap( + &mut self, + #[callback_result] swap_result: Result, + farm_id_str: String, + common_token: u64, + ) { + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder_mut = self.get_strat_mut(&token_id).get_mut(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); + + // Do not panic if err == true, otherwise the slippage update will not be applied + if swap_result.is_err() { + farm_info_mut.increase_slippage(); + log!("ERR_SECOND_SWAP_FAILED"); + return; + } + + // no more rewards to spend + farm_info_mut.last_reward_amount = 0; + // update balance to add liquidity + if common_token == 1 { + // update missing balance + farm_info_mut.available_balance[1] = swap_result.unwrap().0; + } else if common_token == 2 { + // update missing balance + farm_info_mut.available_balance[0] = swap_result.unwrap().0; + } else { + farm_info_mut.available_balance[1] = swap_result.unwrap().0; } + // reset slippage + farm_info_mut.slippage = 100 - MIN_SLIPPAGE_ALLOWED; + // after both swaps succeeded, it's ready to stake + farm_info_mut.next_cycle(); } /// Step 4 /// Get amount of tokens available then stake it - pub fn autocompounds_liquidity_and_stake(&mut self, token_id: String) /* -> Promise */ - { - self.assert_strategy_running(token_id.clone()); - self.is_allowed_account(); - - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); - let compounder = strat.get_mut(); + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + #[private] + pub fn autocompounds_liquidity_and_stake(&mut self, farm_id_str: String) -> Promise { + self.assert_strategy_not_cleared(&farm_id_str); + log!("autocompounds_liquidity_and_stake"); - let pool_id: u64 = compounder.pool_id; + // send reward to contract caller + self.send_reward_to_sentry(farm_id_str, env::predecessor_account_id()) + } - let token1_amount = compounder.available_balance[0]; - let token2_amount = compounder.available_balance[1]; + #[private] + pub fn send_reward_to_sentry(&self, farm_id_str: String, sentry_acc_id: AccountId) -> Promise { + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); - // ensure that in the next run we won't have a balance unless previous steps succeeds - compounder.available_balance[0] = 0u128; - compounder.available_balance[1] = 0u128; - - // Add liquidity - self.call_add_liquidity( - pool_id, - vec![U128(token1_amount), U128(token2_amount)], - None, - ) - // Get the shares - .then(ext_self::callback_stake( - env::current_account_id(), + ext_reward_token::storage_balance_of( + sentry_acc_id.clone(), + farm_info.reward_token.clone(), 0, Gas(10_000_000_000_000), - )) - .and(ext_exchange::get_pool_shares( - pool_id, + ) + .then(ext_self::callback_post_sentry( + farm_id_str, + sentry_acc_id, + farm_info.reward_token, env::current_account_id(), - self.data().exchange_contract_id.clone(), 0, - Gas(10_000_000_000_000), + Gas(240_000_000_000_000), )) - // Update user balance and stake - .then(ext_self::callback_post_get_pool_shares( - token_id, + } + + pub fn callback_post_sentry( + &mut self, + #[callback_result] result: Result, PromiseError>, + farm_id_str: String, + sentry_acc_id: AccountId, + reward_token: AccountId, + ) -> Promise { + // TODO: propagate error + match result { + Ok(balance_op) => match balance_op { + Some(balance) => assert!(balance.total.0 > 1), + _ => { + // let msg = ("ERR: callback post Sentry no balance {:#?} ",balance_op); + let msg = format!( + "{}{:#?}", + "ERR: callback_post_sentry - not enough balance on storage", + balance_op + .unwrap_or(StorageBalance { + total: U128(0), + available: U128(0) + }) + .total + ); + env::panic_str(msg.as_str()); + } + }, + Err(_) => env::panic_str( + "ERR: callback post Sentry - caller not registered to Reward token contract", + ), + } + + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder = self.get_strat_mut(&token_id.to_string()).get_mut(); + + // reset default sentry address and get last earned amount + let amount = compounder + .admin_fees + .sentries + .remove(&env::current_account_id()) + .unwrap(); + + log!("Sending {} to sentry account {}", amount, sentry_acc_id); + + ext_reward_token::ft_transfer( + sentry_acc_id.clone(), + U128(amount), + Some("".to_string()), + reward_token, + 1, + Gas(20_000_000_000_000), + ) + .then(ext_self::callback_post_sentry_mft_transfer( + farm_id_str, + sentry_acc_id, + amount, env::current_account_id(), 0, - Gas(120_000_000_000_000), - )); + Gas(200_000_000_000_000), + )) } + /// Callback to verify that transfer to treasure succeeded #[private] - pub fn callback_stake( - &self, + pub fn callback_post_sentry_mft_transfer( + &mut self, + #[callback_result] ft_transfer_result: Result<(), PromiseError>, + farm_id_str: String, + sentry_id: AccountId, + amount_earned: u128, + ) -> PromiseOrValue { + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + // in the case where the transfer failed, the next cycle will send it plus the new amount earned + if ft_transfer_result.is_err() { + log!("Transfer to sentry failed".to_string()); + + let compounder = self.get_strat_mut(&token_id.to_string()).get_mut(); + + // store amount earned by sentry to be redeemed + compounder + .admin_fees + .sentries + .insert(sentry_id, amount_earned); + } else { + log!("Transfer to sentry succeeded".to_string()); + } + + let strat = self.get_strat(token_id.clone()); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); + + // if farm is ended, there is no more actions to do + if farm_info.state == AutoCompounderState::Ended { + let compounder = self.get_strat_mut(&token_id).get_mut(); + let farm_info = compounder.get_mut_farm_info(farm_id); + farm_info.state = AutoCompounderState::Cleared; + + log!("There farm {} ended. Strategy is now Cleared.", farm_id_str); + return PromiseOrValue::Value(0u64); + } + + let pool_id: u64 = compounder.pool_id; + + let token1_amount = farm_info.available_balance[0]; + let token2_amount = farm_info.available_balance[1]; + + PromiseOrValue::Promise( + ext_exchange::add_liquidity( + pool_id, + vec![U128(token1_amount), U128(token2_amount)], + None, + self.data().exchange_contract_id.clone(), + 970000000000000000000, // TODO: create const to do a meaningful name to this value + Gas(30_000_000_000_000), + ) + .then(ext_self::callback_post_add_liquidity( + farm_id_str.clone(), + env::current_account_id(), + 0, + Gas(10_000_000_000_000), + )) + // Get the shares + .then(ext_exchange::get_pool_shares( + pool_id, + env::current_account_id(), + self.data().exchange_contract_id.clone(), + 0, + Gas(10_000_000_000_000), + )) + // Update user balance and stake + .then(ext_self::callback_post_get_pool_shares( + farm_id_str, + env::current_account_id(), + 0, + Gas(120_000_000_000_000), + )), + ) + } + + #[private] + pub fn callback_post_add_liquidity( + &mut self, #[callback_result] shares_result: Result, + farm_id_str: String, ) -> U128 { assert!(shares_result.is_ok(), "ERR"); - shares_result.unwrap() + + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let compounder_mut = self.get_strat_mut(&token_id).get_mut(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); + + // ensure that in the next run we won't have a balance unless previous steps succeeds + farm_info_mut.available_balance[0] = 0u128; + farm_info_mut.available_balance[1] = 0u128; + + // update owned shares for given seed + let shares_received = shares_result.unwrap().0; + + let total_seed = self.seed_total_amount(token_id); + + log!("shares received {}. total {}", shares_received, total_seed); + + let data = self.data_mut(); + + data.seed_id_amount + .insert(&seed_id, &(total_seed + shares_received)); + + U128(shares_received) } /// Receives shares from auto-compound and stake it @@ -537,31 +949,30 @@ impl Contract { #[private] pub fn callback_post_get_pool_shares( &mut self, - #[callback_unwrap] minted_shares_result: U128, #[callback_result] total_shares_result: Result, - token_id: String, + farm_id_str: String, ) { assert!(total_shares_result.is_ok(), "ERR"); - let shares_amount = minted_shares_result.0; - let strat = self.get_strat_mut(&token_id); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder_mut = self.get_strat_mut(&token_id.to_string()).get_mut(); - let compounder = strat.get_mut(); + compounder_mut.harvest_timestamp = env::block_timestamp_ms(); - if shares_amount > 0 { - let mut total_shares: u128 = 0; + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); - for (_, balance) in compounder.user_shares.iter() { - total_shares += balance.total; - } - - compounder.balance_update(total_shares, shares_amount); - }; + farm_info_mut.next_cycle(); let accumulated_shares = total_shares_result.unwrap().0; // Prevents failing on stake if below minimum deposit - if accumulated_shares < compounder.seed_min_deposit.into() { + let min_deposit = compounder_mut.seed_min_deposit; + log!( + "min_deposit {} and shares {}", + min_deposit.0, + accumulated_shares + ); + if accumulated_shares < min_deposit.0 { log!( "The current number of shares {} is below minimum deposit", accumulated_shares @@ -571,9 +982,9 @@ impl Contract { self.call_stake( self.data().farm_contract_id.clone(), - token_id, + token_id.to_string(), U128(accumulated_shares), - "".to_string(), + "\"Free\"".to_string(), ); } } diff --git a/fluxus-safe/src/auto_compounder.rs b/fluxus-safe/src/auto_compounder.rs index 319653b..d0af4d2 100644 --- a/fluxus-safe/src/auto_compounder.rs +++ b/fluxus-safe/src/auto_compounder.rs @@ -1,72 +1,97 @@ use crate::*; -#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, PartialEq, Clone)] -#[serde(crate = "near_sdk::serde")] -pub struct SharesBalance { - /// stores the amount given address deposited - pub deposited: u128, - /// stores the amount given address deposited plus the earned shares - pub total: u128, -} +const MAX_SLIPPAGE_ALLOWED: u128 = 20; -// #[derive(BorshSerialize, BorshDeserialize)] -#[derive(Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Clone)] +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(crate = "near_sdk::serde")] -pub struct AutoCompounder { - // Struct that maps addresses to its currents shares added plus the received - // from the auto-compound strategy - pub user_shares: HashMap, - - // Fee applied to each round of auto-compound - pub protocol_fee: u128, - - // Keeps tracks of how much shares the contract gained from the auto-compound - pub protocol_shares: u128, - - // State is used to update the contract to a Paused/Running state +pub struct StratFarmInfo { + /// State is used to update the contract to a Paused/Running state pub state: AutoCompounderState, + /// Used to keep track of the current stage of the auto-compound cycle + pub cycle_stage: AutoCompounderCycle, + /// Slippage applied to swaps, range from 0 to 100. /// Defaults to 5%. The value will be computed as 100 - slippage pub slippage: u128, - // Used to keep track of the rewards received from the farm during auto-compound cycle + /// Used to keep track of the rewards received from the farm during auto-compound cycle pub last_reward_amount: u128, /// Used to keep track of the owned amount from fee of the token reward /// This will be used to store owned amount if ft_transfer to treasure fails pub last_fee_amount: u128, - // Address of the first token used by pool - pub token1_address: AccountId, - - // Address of the token used by the pool - pub token2_address: AccountId, - - // Pool used to swap the reward received by the token used to add liquidity + /// Pool used to swap the reward received by the token used to add liquidity pub pool_id_token1_reward: u64, - // Pool used to swap the reward received by the token used to add liquidity + /// Pool used to swap the reward received by the token used to add liquidity pub pool_id_token2_reward: u64, - // Address of the reward token given by the farm + /// Address of the reward token given by the farm pub reward_token: AccountId, - // Store balance of available token1 and token2 - // obs: would be better to have it in as a LookupMap, but Serialize and Clone is not available for it + /// Store balance of available token1 and token2 + /// obs: would be better to have it in as a LookupMap, but Serialize and Clone is not available for it pub available_balance: Vec, - // Farm used to auto-compound - pub farm_id: String, + /// Farm used to auto-compound + pub id: String, +} + +impl StratFarmInfo { + pub(crate) fn next_cycle(&mut self) { + match self.cycle_stage { + AutoCompounderCycle::ClaimReward => self.cycle_stage = AutoCompounderCycle::Withdrawal, + AutoCompounderCycle::Withdrawal => self.cycle_stage = AutoCompounderCycle::Swap, + AutoCompounderCycle::Swap => self.cycle_stage = AutoCompounderCycle::Stake, + AutoCompounderCycle::Stake => self.cycle_stage = AutoCompounderCycle::ClaimReward, + } + } + + pub fn increase_slippage(&mut self) { + if 100u128 - self.slippage < MAX_SLIPPAGE_ALLOWED { + // increment slippage + self.slippage -= 4; + + log!( + "Slippage updated to {}. It will applied in the next call", + self.slippage + ); + } else { + self.state = AutoCompounderState::Ended; + log!("Slippage too high. State was updated to Ended"); + } + } +} - // Pool used to add liquidity and farming +// #[derive(BorshSerialize, BorshDeserialize)] +#[derive(Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct AutoCompounder { + /// Fees struct to be distribute at each round of compound + pub admin_fees: AdminFees, + + /// Address of the first token used by pool + pub token1_address: AccountId, + + /// Address of the token used by the pool + pub token2_address: AccountId, + + /// Pool used to add liquidity and farming pub pool_id: u64, - // Min LP amount accepted by the farm for stake + /// Min LP amount accepted by the farm for stake pub seed_min_deposit: U128, - // Format expected by the farm to claim and withdraw rewards + /// Format expected by the farm to claim and withdraw rewards pub seed_id: String, + + /// Store all farms that were used to compound by some token_id + pub farms: Vec, + + /// Latest harvest timestamp + pub harvest_timestamp: u64, } #[derive(Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Clone)] @@ -89,9 +114,10 @@ impl From<&AutoCompounderState> for String { } } -#[derive(BorshSerialize, BorshDeserialize, Clone)] +#[derive(Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] pub enum AutoCompounderCycle { - Reward, + ClaimReward, Withdrawal, Swap, Stake, @@ -100,7 +126,7 @@ pub enum AutoCompounderCycle { impl From<&AutoCompounderCycle> for String { fn from(cycle: &AutoCompounderCycle) -> Self { match *cycle { - AutoCompounderCycle::Reward => String::from("Reward"), + AutoCompounderCycle::ClaimReward => String::from("Reward"), AutoCompounderCycle::Withdrawal => String::from("Withdrawal"), AutoCompounderCycle::Swap => String::from("Swap"), AutoCompounderCycle::Stake => String::from("Stake"), @@ -111,106 +137,85 @@ impl From<&AutoCompounderCycle> for String { /// Auto-compounder internal methods impl AutoCompounder { pub(crate) fn new( - protocol_fee: u128, + strategy_fee: u128, + treasury: AccountFee, + strat_creator: AccountFee, + sentry_fee: u128, token1_address: AccountId, token2_address: AccountId, - pool_id_token1_reward: u64, - pool_id_token2_reward: u64, - reward_token: AccountId, - farm_id: String, pool_id: u64, seed_id: String, seed_min_deposit: U128, ) -> Self { + let admin_fee = AdminFees::new(strat_creator, sentry_fee, strategy_fee); + Self { - user_shares: HashMap::new(), - protocol_fee, - protocol_shares: 0u128, - state: AutoCompounderState::Running, - slippage: 95u128, - last_reward_amount: 0u128, - last_fee_amount: 0u128, + admin_fees: admin_fee, token1_address, token2_address, - pool_id_token1_reward, - pool_id_token2_reward, - reward_token, - available_balance: vec![0u128, 0u128], - farm_id, pool_id, seed_min_deposit, seed_id, + farms: Vec::new(), + harvest_timestamp: 0u64, } } - /// Update user balances based on the user's percentage in the contract. - pub(crate) fn balance_update(&mut self, total: u128, shares_reward: u128) { - log!("new_shares_quantity is equal to {}", shares_reward); - - let mut shares_distributed: U256 = U256::from(0u128); + pub(crate) fn compute_fees(&mut self, reward_amount: u128) -> (u128, u128, u128, u128) { + // apply fees to reward amount + let percent = Percentage::from(self.admin_fees.strategy_fee); + let all_fees_amount = percent.apply_to(reward_amount); - for (account, mut shares) in self.user_shares.clone() { - let val = shares.total; - let acc_percentage = U256::from(val) * U256::from(F) / U256::from(total); + let percent = Percentage::from(self.admin_fees.sentries_fee); + let sentry_amount = percent.apply_to(all_fees_amount); - let casted_reward = U256::from(shares_reward) * acc_percentage; + let percent = Percentage::from(self.admin_fees.strat_creator.fee_percentage); + let strat_creator_amount = percent.apply_to(all_fees_amount); + let treasury_amount = all_fees_amount - sentry_amount - strat_creator_amount; - let earned_shares: U256 = casted_reward / U256::from(F); + let remaining_amount = + reward_amount - treasury_amount - sentry_amount - strat_creator_amount; - shares_distributed += earned_shares; - - let new_user_balance: u128 = (U256::from(val) + earned_shares).as_u128(); - - shares.total = new_user_balance; + ( + remaining_amount, + treasury_amount, + sentry_amount, + strat_creator_amount, + ) + } - self.user_shares.insert(account.clone(), shares); + pub fn get_farm_info(&self, farm_id: &str) -> StratFarmInfo { + for farm in self.farms.iter() { + if farm.id == farm_id { + return farm.clone(); + } } - let residue: u128 = shares_reward - shares_distributed.as_u128(); - log!("Shares residue: {}", residue); + panic!("Farm does not exist") } - pub(crate) fn increment_user_shares(&mut self, account_id: &AccountId, shares: Balance) { - let initial_balance = &mut SharesBalance { - deposited: 0u128, - total: 0u128, - }; - let mut user_shares = self - .user_shares - .get(account_id) - .unwrap_or(initial_balance) - .clone(); - - let user_lps = user_shares.total; - - // TODO: is it possible to use get_mut on user_shares, to avoid using insert? - - if user_lps > 0 { - user_shares.deposited += shares; - user_shares.total += shares; - self.user_shares.insert(account_id.clone(), user_shares); - } else { - user_shares.deposited = shares; - user_shares.total = shares; - self.user_shares.insert(account_id.clone(), user_shares); - }; + pub fn get_mut_farm_info(&mut self, farm_id: String) -> &mut StratFarmInfo { + for farm in self.farms.iter_mut() { + if farm.id == farm_id { + return farm; + } + } + + panic!("Farm does not exist") } - pub(crate) fn decrement_user_shares(&mut self, account_id: &AccountId, shares: Balance) { - let mut user_shares = self.user_shares.get(account_id).unwrap().clone(); - let new_shares: u128 = user_shares.total - shares; - log!( - "{} had {} and now has {}", - account_id, - user_shares.total, - new_shares - ); - - // The following code resets the initial deposit - // The earned_shares will return how much the user has earned after the withdraw, not the deposit - user_shares.deposited = new_shares; - user_shares.total = new_shares; - self.user_shares.insert(account_id.clone(), user_shares); + /// Iterate through farms inside a compounder + /// if `rewards_map` contains the same token than the strat, an reward > 0, + /// then updates the strat to the next cycle, to avoid claiming the seed multiple times + /// TODO: what if there are multiple farms with the same token_reward? + pub fn update_strats_by_seed(&mut self, rewards_map: HashMap) { + for farm in self.farms.iter_mut() { + if let Some(reward_earned) = rewards_map.get(&farm.reward_token.to_string()) { + if reward_earned.0 > 0 { + farm.last_reward_amount += reward_earned.0; + } + } + } } } @@ -224,158 +229,29 @@ pub enum VersionedCompounder { } impl VersionedCompounder { + #[allow(dead_code)] pub fn new( - protocol_fee: u128, + strategy_fee: u128, + treasury: AccountFee, + strat_creator: AccountFee, + sentry_fee: u128, token1_address: AccountId, token2_address: AccountId, - pool_id_token1_reward: u64, - pool_id_token2_reward: u64, - reward_token: AccountId, - farm_id: String, pool_id: u64, seed_id: String, seed_min_deposit: U128, ) -> Self { + let admin_fee = AdminFees::new(strat_creator, sentry_fee, strategy_fee); + VersionedCompounder::V101(AutoCompounder { - user_shares: HashMap::new(), - protocol_fee, - protocol_shares: 0u128, - state: AutoCompounderState::Running, - slippage: 95u128, - last_reward_amount: 0u128, - last_fee_amount: 0u128, + admin_fees: admin_fee, token1_address, token2_address, - pool_id_token1_reward, - pool_id_token2_reward, - reward_token, - available_balance: vec![0u128, 0u128], - farm_id, pool_id, seed_min_deposit, seed_id, + farms: Vec::new(), + harvest_timestamp: 0u64, }) } } - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use std::hash::Hash; - - use super::*; - use near_sdk::test_utils::VMContextBuilder; - use near_sdk::testing_env; - - fn get_context() -> VMContextBuilder { - let mut builder = VMContextBuilder::new(); - builder - .current_account_id(to_account_id("auto_compounder.near")) - .signer_account_id(to_account_id("auto_compounder.near")) - .predecessor_account_id(to_account_id("auto_compounder.near")); - builder - } - - pub fn to_account_id(value: &str) -> AccountId { - value.parse().unwrap() - } - - fn create_contract() -> Contract { - let contract = Contract::new( - to_account_id("auto_compounder.near"), - String::from("eth.near").parse().unwrap(), - String::from("dai.near").parse().unwrap(), - String::from("treasure.near").parse().unwrap(), - ); - - contract - } - - // fn create_strat(mut safe_contract: Contract) {} - - // #[test] - // fn test_balance_update() { - // let context = get_context(); - // testing_env!(context.build()); - - // let mut contract = create_contract(); - - // let near: u128 = 1_000_000_000_000_000_000_000_000; // 1 N - - // let acc1 = to_account_id("alice.near"); - // let shares1 = near.clone(); - - // let acc2 = to_account_id("bob.near"); - // let shares2 = near.clone() * 3; - - // let token1_address = String::from("eth.near").parse().unwrap(); - // let token2_address = String::from("dai.near").parse().unwrap(); - // let pool_id_token1_reward = 0; - // let pool_id_token2_reward = 1; - // let reward_token = String::from("usn.near").parse().unwrap(); - // let farm = "0".to_string(); - // let pool_id = 0; - // let seed_min_deposit = U128(10); - - // contract.create_auto_compounder( - // token1_address, - // token2_address, - // pool_id_token1_reward, - // pool_id_token2_reward, - // reward_token, - // farm, - // pool_id, - // seed_min_deposit, - // ); - - // let token_id = String::from(":0"); - // // add initial balance for accounts - // contract - // .strategies - // .get_mut(&token_id) - // .unwrap() - // .user_shares - // .insert(acc1.clone(), shares1); - - // contract - // .strategies - // .get_mut(&token_id) - // .unwrap() - // .user_shares - // .insert(acc2.clone(), shares2); - - // let total_shares: u128 = shares1 + shares2; - - // // distribute shares between accounts - // contract - // .strategies - // .get_mut(&token_id) - // .unwrap() - // .balance_update(total_shares, near); - - // // assert account 1 earned 25% from reward shares - // let acc1_updated_shares = contract - // .strategies - // .get(&token_id) - // .unwrap() - // .user_shares - // .get(&acc1) - // .unwrap(); - // assert_eq!( - // *acc1_updated_shares, 1250000000000000000000000u128, - // "ERR_BALANCE_UPDATE" - // ); - - // // assert account 2 earned 75% from reward shares - // let acc2_updated_shares = contract - // .strategies - // .get(&token_id) - // .unwrap() - // .user_shares - // .get(&acc2) - // .unwrap(); - // assert_eq!( - // *acc2_updated_shares, 3750000000000000000000000u128, - // "ERR_BALANCE_UPDATE" - // ); - // } -} diff --git a/fluxus-safe/src/errors.rs b/fluxus-safe/src/errors.rs index 6a97d69..2f81ae8 100644 --- a/fluxus-safe/src/errors.rs +++ b/fluxus-safe/src/errors.rs @@ -1,35 +1,3 @@ -// Storage errors. - -pub const ERR10_ACC_NOT_REGISTERED: &str = "E10: account not registered"; -pub const ERR11_INSUFFICIENT_STORAGE: &str = "E11: insufficient $NEAR storage deposit"; -pub const ERR12_TOKEN_NOT_WHITELISTED: &str = "E12: token not whitelisted"; -pub const ERR13_LP_NOT_REGISTERED: &str = "E13: LP not registered"; -pub const ERR14_LP_ALREADY_REGISTERED: &str = "E14: LP already registered"; - -// Accounts. pub const ERR21_TOKEN_NOT_REG: &str = "E21: token not registered"; -pub const ERR22_NOT_ENOUGH_TOKENS: &str = "E22: not enough tokens in deposit"; -// pub const ERR23_NOT_ENOUGH_NEAR: &str = "E23: not enough NEAR in deposit"; -pub const ERR24_NON_ZERO_TOKEN_BALANCE: &str = "E24: non-zero token balance"; -pub const ERR25_CALLBACK_POST_WITHDRAW_INVALID: &str = - "E25: expected 1 promise result from withdraw"; -// [AUDIT_05] -// pub const ERR26_ACCESS_KEY_NOT_ALLOWED: &str = "E26: access key not allowed"; -pub const ERR27_DEPOSIT_NEEDED: &str = - "E27: attach 1yN to swap tokens not in whitelist"; -pub const ERR28_WRONG_MSG_FORMAT: &str = "E28: Illegal msg in ft_transfer_call"; -pub const ERR29_ILLEGAL_WITHDRAW_AMOUNT: &str = "E29: Illegal withdraw amount"; - -// Liquidity operations. - -pub const ERR31_ZERO_AMOUNT: &str = "E31: adding zero amount"; -pub const ERR32_ZERO_SHARES: &str = "E32: minting zero shares"; -// [AUDIT_07] pub const ERR33_TRANSFER_TO_SELF: &str = "E33: transfer to self"; -// Action result. - -pub const ERR41_WRONG_ACTION_RESULT: &str = "E41: wrong action result type"; - -// Contract Level -pub const ERR51_CONTRACT_PAUSED: &str = "E51: contract paused"; \ No newline at end of file diff --git a/fluxus-safe/src/external_contracts.rs b/fluxus-safe/src/external_contracts.rs index ede9213..974c6cf 100644 --- a/fluxus-safe/src/external_contracts.rs +++ b/fluxus-safe/src/external_contracts.rs @@ -46,6 +46,27 @@ pub struct FarmInfo { pub beneficiary_reward: U128, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct TermsBoost { + pub reward_token: String, + pub start_at: u128, + pub daily_reward: U128, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct FarmInfoBoost { + pub farm_id: FarmId, + pub terms: TermsBoost, + pub total_reward: U128, + pub distributed_at: String, + pub distributed_reward: U128, + pub claimed_reward: U128, + pub amount_of_beneficiary: U128, + pub status: String, +} + type SeedId = String; type FarmId = String; @@ -62,11 +83,24 @@ pub trait Farming { fn claim_reward_by_seed(&mut self, seed_id: String); fn claim_reward_by_farm(&mut self, farm_id: String); fn withdraw_seed(&mut self, seed_id: String, amount: U128, msg: String); + /// Boost contract + fn unlock_and_withdraw_seed( + &mut self, + seed_id: String, + unlock_amount: U128, + withdraw_amount: U128, + ) -> bool; fn withdraw_reward(&mut self, token_id: String, amount: U128, unregister: String) -> Promise; fn get_reward(&mut self, account_id: AccountId, token_id: AccountId) -> U128; fn get_unclaimed_reward(&mut self, account_id: AccountId, farm_id: String) -> U128; + fn get_unclaimed_rewards( + &mut self, + farmer_id: AccountId, + seed_id: String, + ) -> HashMap; fn list_user_seeds(&self, account_id: AccountId) -> HashMap; fn list_farms_by_seed(&self, seed_id: SeedId) -> Vec; + fn list_seed_farms(&self, seed_id: SeedId) -> Vec; } // Ref exchange functions that we need to call inside the auto_compounder. @@ -95,7 +129,7 @@ pub trait RefExchange { amounts: Vec, min_amounts: Option>, ) -> U128; - fn swap(&mut self, actions: Vec, referral_id: Option); + fn swap(&mut self, actions: Vec, referral_id: Option) -> U128; fn mft_transfer_call( &mut self, receiver_id: AccountId, @@ -129,7 +163,10 @@ pub trait ExtRewardToken { fn ft_transfer_call( &mut self, receiver_id: AccountId, - amount: String, + amount: U128, msg: String, ) -> PromiseOrValue; + fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option); + fn ft_balance_of(&self, account_id: AccountId) -> U128; + fn storage_balance_of(&self, account_id: AccountId) -> Option; } diff --git a/fluxus-safe/src/fluxus_strat.rs b/fluxus-safe/src/fluxus_strat.rs index 7929dbd..046498b 100644 --- a/fluxus-safe/src/fluxus_strat.rs +++ b/fluxus-safe/src/fluxus_strat.rs @@ -6,7 +6,7 @@ use near_sdk::{ serde::{Deserialize, Serialize}, }; -pub(crate) type StratId = String; +// pub(crate) type StratId = String; /// Generic Strategy, providing wrapper around different implementations of strategies. /// Allows to add new types of strategies just by adding extra item in the enum @@ -25,15 +25,6 @@ impl VersionedStrategy { } } - // TODO: impl - // pub fn strategy_cycle(&mut self) { - // match self { - // VersionedStrategy::AutoCompounder(strat) => { - // strat.strategy_cycle(); - // } - // } - // } - // TODO: impl // pub fn get_strategy_id(&self) -> StratId { // match self { @@ -42,6 +33,7 @@ impl VersionedStrategy { // } /// update method in order to upgrade strategy + #[allow(unreachable_patterns)] pub fn upgrade(&self) -> Self { match self { VersionedStrategy::AutoCompounder(compounder) => { @@ -52,6 +44,7 @@ impl VersionedStrategy { } /// update method in order to upgrade strategy + #[allow(unreachable_patterns)] pub fn need_upgrade(&self) -> bool { match self { Self::AutoCompounder(_) => false, @@ -59,28 +52,30 @@ impl VersionedStrategy { } } - // Return the farm or liquidity pool or token( other kinds of strategy) this strategy accepts - pub fn get_token_id(&self) -> String { - match self { - VersionedStrategy::AutoCompounder(strat) => strat.farm_id.clone(), - _ => unimplemented!(), - } - } + // // Return the farm or liquidity pool or token( other kinds of strategy) this strategy accepts + // #[allow(unreachable_patterns)] + // pub fn get_token_id(&self) -> String { + // match self { + // VersionedStrategy::AutoCompounder(strat) => strat.farm_id.clone(), + // _ => unimplemented!(), + // } + // } + #[allow(unreachable_patterns)] pub fn get(self) -> AutoCompounder { match self { VersionedStrategy::AutoCompounder(compounder) => compounder, _ => unimplemented!(), } } - + #[allow(unreachable_patterns)] pub fn get_ref(&self) -> &AutoCompounder { match self { VersionedStrategy::AutoCompounder(compounder) => compounder, _ => unimplemented!(), } } - + #[allow(unreachable_patterns)] pub fn get_mut(&mut self) -> &mut AutoCompounder { match self { VersionedStrategy::AutoCompounder(compounder) => compounder, @@ -90,11 +85,11 @@ impl VersionedStrategy { } impl Contract { - pub fn get_strat(&self, token_id: &String) -> VersionedStrategy { + pub fn get_strat(&self, token_id: String) -> VersionedStrategy { let strat = self .data() .strategies - .get(token_id) + .get(&token_id) .expect(ERR21_TOKEN_NOT_REG); if strat.need_upgrade() { @@ -104,6 +99,24 @@ impl Contract { } } + pub fn get_compounder(&self, farm_id_str: &str) -> &AutoCompounder { + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let strat = self + .data() + .strategies + .get(&token_id) + .expect(ERR21_TOKEN_NOT_REG); + + strat.get_ref() + + // if strat.need_upgrade() { + // strat.upgrade() + // } else { + // strat.clone() + // } + } + pub fn get_strat_mut(&mut self, token_id: &String) -> &mut VersionedStrategy { let strat = self .data_mut() diff --git a/fluxus-safe/src/lib.rs b/fluxus-safe/src/lib.rs index 4a5c315..1450ffd 100644 --- a/fluxus-safe/src/lib.rs +++ b/fluxus-safe/src/lib.rs @@ -14,7 +14,7 @@ use near_sdk::json_types::U128; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{ assert_one_yocto, env, ext_contract, log, near_bindgen, require, AccountId, Balance, - BorshStorageKey, Gas, PanicOnDefault, Promise, PromiseResult, + BorshStorageKey, Gas, PanicOnDefault, Promise, PromiseOrValue, PromiseResult, }; use percentage::Percentage; @@ -26,15 +26,15 @@ mod storage_impl; mod token_receiver; mod external_contracts; -use external_contracts::*; +pub use external_contracts::*; mod utils; mod errors; use crate::errors::*; -mod auto_compounder; -use auto_compounder::*; +pub mod auto_compounder; +pub use auto_compounder::*; mod actions_of_compounder; @@ -47,13 +47,23 @@ mod actions_of_strat; mod owner; +pub mod admin_fee; +pub use admin_fee::*; + +mod multi_fungible_token; + #[derive(BorshStorageKey, BorshSerialize)] pub(crate) enum StorageKey { Accounts, Whitelist, AccountTokens { account_id: AccountId }, - Shares { pool_id: u64 }, Guardian, + NearDeposited, + UsersBalanceByShare, + TotalSupplyByShare, + SeedIdAmount, + SeedRegister { fft_share: String }, + Strategy, } // TODO: update this to newer version, following AutoCompounderState @@ -83,8 +93,8 @@ pub struct ContractData { /// Set of guardians. guardians: UnorderedSet, - // Keeps tracks of how much shares the contract gained from the auto-compound - protocol_shares: u128, + /// Fees earned by the DAO + treasury: AccountFee, // Keeps tracks of accounts that send coins to this contract accounts: LookupMap, @@ -99,17 +109,30 @@ pub struct ContractData { state: RunningState, // Used by storage_impl and account_deposit to keep track of NEAR deposit in this contract - users_total_near_deposited: HashMap, + users_total_near_deposited: LookupMap, + + ///It is a map that store the fft_share and a map of users and their balance. + /// illustration: map(fft_share[i], map(user[i], balance[i])). + users_balance_by_fft_share: LookupMap>, + + ///Store the fft_share total_supply for each seed_id. + total_supply_by_fft_share: LookupMap, + + ///Store the fft_share for each seed_id. + /// TODO: Change HashMap for LookupMap as it is more gas efficient + fft_share_by_seed_id: HashMap, + + ///Store the fft_share for each seed_id. + seed_id_amount: LookupMap, // Contract address of the exchange used + //TODO: Move it inside the strategy exchange_contract_id: AccountId, // Contract address of the farm used + //TODO: Move it inside the strategy farm_contract_id: AccountId, - // Contract address to receive earned shares from fee - treasure_contract_id: AccountId, - // Pools used to harvest, in the ":X" format token_ids: Vec, @@ -122,7 +145,7 @@ pub trait Callbacks { fn call_get_pool_shares(&mut self, pool_id: u64, account_id: AccountId) -> String; fn call_swap( &self, - pool_id_to_swap: u64, + pool_id: u64, token_in: AccountId, token_out: AccountId, amount_in: Option, @@ -140,21 +163,31 @@ pub trait Callbacks { token_id: String, account_id: AccountId, amount: Balance, + fft_shares: Balance, ); fn callback_get_deposits(&self) -> Promise; fn callback_get_tokens_return(&self) -> (U128, U128); fn callback_get_token_return(&self, common_token: u64, amount_token: U128) -> (U128, U128); - fn callback_stake(&mut self, #[callback_result] shares_result: Result); + fn callback_post_add_liquidity( + &mut self, + #[callback_result] shares_result: Result, + farm_id_str: String, + ); fn callback_post_get_pool_shares( &mut self, - #[callback_unwrap] minted_shares_result: U128, #[callback_result] total_shares_result: Result, + farm_id_str: String, + ); + fn callback_stake_result( + &mut self, + #[callback_result] transfer_result: Result, token_id: String, + account_id: AccountId, + shares: u128, ); - fn callback_stake_result(&mut self, token_id: String, account_id: AccountId, shares: u128); fn swap_to_auto( &mut self, - token_id: String, + farm_id_str: String, amount_in_1: U128, amount_in_2: U128, common_token: u64, @@ -165,33 +198,58 @@ pub trait Callbacks { token_id: String, account_id: AccountId, ); - fn balance_update(&mut self, vec: HashMap, shares: String); fn get_tokens_return( &self, - #[callback_result] ft_transfer_result: Result<(), PromiseError>, - token_id: String, + farm_id_str: String, amount_token_1: U128, amount_token_2: U128, common_token: u64, ) -> Promise; fn callback_post_withdraw( &mut self, - #[callback_result] withdraw_result: Result, - token_id: String, + #[callback_result] withdraw_result: Result, + farm_id_str: String, ) -> Promise; - fn callback_post_mft_transfer( + fn callback_post_treasury_mft_transfer( #[callback_result] ft_transfer_result: Result<(), PromiseError>, - token_id: String, + ); + fn callback_post_sentry_mft_transfer( + &mut self, + #[callback_result] ft_transfer_result: Result<(), PromiseError>, + farm_id_str: String, + sentry_id: AccountId, + amount_earned: u128, ); fn callback_post_claim_reward( &self, #[callback_result] claim_result: Result<(), PromiseError>, - token_id: String, + farm_id_str: String, + reward_amount: U128, + rewards_map: HashMap, ) -> Promise; + fn callback_post_first_swap( + &mut self, + #[callback_result] swap_result: Result, + farm_id_str: String, + common_token: u64, + amount_in: U128, + token_min_out: U128, + ) -> PromiseOrValue; + fn callback_post_swap( + &mut self, + #[callback_result] swap_result: Result, + farm_id_str: String, + common_token: u64, + ); fn callback_post_get_unclaimed_reward( &self, #[callback_result] claim_result: Result<(), PromiseError>, - token_id: String, + farm_id_str: String, + ) -> PromiseOrValue; + fn callback_post_unclaimed_rewards( + &self, + #[callback_result] rewards_result: Result, PromiseError>, + reward_token: AccountId, ); fn callback_get_pool_shares( &self, @@ -202,12 +260,27 @@ pub trait Callbacks { ) -> Promise; fn callback_list_farms_by_seed( &self, - #[callback_result] farms_result: Result, PromiseError>, - token_id: String, - farm_id: String, + #[callback_result] farms_result: Result, PromiseError>, + farm_id_str: String, ) -> Promise; + fn callback_post_ft_transfer( + &mut self, + #[callback_result] exchange_transfer_result: Result, + farm_id_str: String, + ); + fn callback_post_creator_ft_transfer( + &mut self, + #[callback_result] strat_creator_transfer_result: Result, + token_id: String, + ); + fn callback_post_sentry( + &self, + #[callback_result] result: Result, + farm_id_str: String, + sentry_acc_id: AccountId, + reward_token: AccountId, + ); } -const F: u128 = 100000000000000000000000000000; // rename this const construct_uint! { /// 256-bit unsigned integer. @@ -222,15 +295,45 @@ impl Contract { _ => env::panic_str("E51: contract paused"), }; } - fn assert_strategy_running(&self, token_id: String) { + + /// Assert that the farm_id_str is valid, meaning that the farm is Running + fn assert_strategy_not_cleared(&self, farm_id_str: &str) { self.assert_contract_running(); - let strat = self.get_strat(&token_id); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); - match strat.get().state { - AutoCompounderState::Running => (), - _ => env::panic_str("E51: strategy ended"), - }; + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + + for farm in compounder.farms.iter() { + if farm.id == farm_id { + match farm.state { + AutoCompounderState::Running => (), + AutoCompounderState::Ended => (), + _ => env::panic_str("E51: strategy ended"), + }; + } + } + } + + // TODO: rename this method + /// Ensures that at least one strategy is running for given token_id + fn assert_token_id(&self, token_id: String) { + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + + let mut has_running_strategy = false; + + for farm in compounder.farms.iter() { + if farm.state == AutoCompounderState::Running { + has_running_strategy = true; + break; + } + } + + if !has_running_strategy { + panic!("There is no running strategy for this pool") + } } /// wrap token_id into correct format in MFT standard @@ -265,20 +368,29 @@ impl Contract { assert!(!env::state_exists(), "Already initialized"); let allowed_accounts: Vec = vec![env::current_account_id()]; + let treasury: AccountFee = AccountFee { + account_id: treasure_contract_id, + fee_percentage: 10, //TODO: the treasury fee_percentage can be removed from here as the treasury contract will receive all the fees amount that won't be sent to strat_creator or sentry + // The breakdown of amount for Stakers, operations and treasury will be dealt with inside the treasury contract + current_amount: 0u128, + }; + Self { data: VersionedContractData::V0001(ContractData { owner_id, guardians: UnorderedSet::new(StorageKey::Guardian), - protocol_shares: 0u128, + treasury, accounts: LookupMap::new(StorageKey::Accounts), allowed_accounts, whitelisted_tokens: UnorderedSet::new(StorageKey::Whitelist), state: RunningState::Running, - // TODO: remove this - users_total_near_deposited: HashMap::new(), + users_total_near_deposited: LookupMap::new(StorageKey::NearDeposited), + users_balance_by_fft_share: LookupMap::new(StorageKey::UsersBalanceByShare), + total_supply_by_fft_share: LookupMap::new(StorageKey::TotalSupplyByShare), + fft_share_by_seed_id: HashMap::new(), + seed_id_amount: LookupMap::new(StorageKey::SeedIdAmount), exchange_contract_id, farm_contract_id, - treasure_contract_id, /// List of all the pools. token_ids: Vec::new(), strategies: HashMap::new(), @@ -287,14 +399,26 @@ impl Contract { } } +/// Splits farm_id_str into token_id and farm_id +/// Returns seed_id, token_id, farm_id (exchange@pool_id, farm_id) +pub fn get_ids_from_farm(farm_id_str: String) -> (String, String, String) { + let ids: Vec<&str> = farm_id_str.split('#').collect(); + let token_id: Vec<&str> = ids[0].split('@').collect(); + + let token_id_wrapped = format!(":{}", token_id[1]); + + (ids[0].to_owned(), token_id_wrapped, ids[1].to_owned()) +} + impl Contract { + #[allow(unreachable_patterns)] fn data(&self) -> &ContractData { match &self.data { VersionedContractData::V0001(data) => data, _ => unimplemented!(), } } - + #[allow(unreachable_patterns)] fn data_mut(&mut self) -> &mut ContractData { match &mut self.data { VersionedContractData::V0001(data) => data, @@ -311,6 +435,6 @@ impl Contract { } fn treasure_acc(&self) -> AccountId { - self.data().treasure_contract_id.clone() + self.data().treasury.account_id.clone() } } diff --git a/fluxus-safe/src/multi_fungible_token.rs b/fluxus-safe/src/multi_fungible_token.rs new file mode 100644 index 0000000..f0cc2cc --- /dev/null +++ b/fluxus-safe/src/multi_fungible_token.rs @@ -0,0 +1,537 @@ +use crate::*; + +#[ext_contract(ext_share_token_receiver)] +pub trait MFTTokenReceiver { + fn mft_on_transfer( + &mut self, + token_id: String, + sender_id: AccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue; +} +#[ext_contract(ext_self)] +trait MFTTokenResolver { + fn mft_resolve_transfer( + &mut self, + token_id: String, + sender_id: AccountId, + receiver_id: AccountId, + amount: U128, + ) -> U128; +} + +pub const NO_DEPOSIT: u128 = 0; +pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(20_000_000_000_000); +pub const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas(45_000_000_000_000); + +#[near_bindgen] +impl Contract { + //Return the FFT token to a seed_id TODO: enhance name of fft tokens they should be named fft_seed_{seed_id} + pub fn fft_token_seed_id(&self, seed_id: String) -> String { + let data = self.data(); + let fft_name: String = if let Some(fft_resp) = data.fft_share_by_seed_id.get(&seed_id) { + fft_resp.to_owned() + } else { + env::panic_str("E1: seed_id doesn't exist"); + }; + fft_name + } + ///Return the u128 amount of an user for an specific fft_share (ref lp token). + pub fn users_fft_share_amount(&self, fft_share: String, user: String) -> u128 { + let map = self.data().users_balance_by_fft_share.get(&fft_share); + if let Some(shares) = map { + if let Some(user_balance) = shares.get(&user) { + return user_balance; + } + } + + 0 + } + /// Return the u128 amount a user has in seed_id. + pub fn user_share_seed_id(&self, seed_id: String, user: String) -> u128 { + let data = self.data(); + let fft_name: String = if let Some(fft_resp) = data.fft_share_by_seed_id.get(&seed_id) { + fft_resp.to_owned() + } else { + env::panic_str("E1: seed_id doesn't exist"); + }; + + let user_fft_shares = self.users_fft_share_amount(fft_name.clone(), user); + let total_fft = self.total_supply_amount(fft_name); + let total_seed = self.data().seed_id_amount.get(&seed_id).unwrap_or_default(); + log!( + "user_fft {} total fft {} total seed {}", + user_fft_shares, + total_fft, + total_seed + ); + if total_fft == 0_u128 || total_seed == 0 || user_fft_shares == 0 { + 0 + } else { + (U256::from(user_fft_shares) * U256::from(total_seed) / U256::from(total_fft)).as_u128() + } + } + + ///Register a seed into the users_balance_by_fft_share + pub fn register_seed(&mut self, fft_share: String) { + let temp = LookupMap::new(StorageKey::SeedRegister { + fft_share: fft_share.clone(), + }); + self.data_mut() + .users_balance_by_fft_share + .insert(&fft_share, &temp); + } + + pub fn seed_total_amount(&self, token_id: String) -> u128 { + let mut id = token_id; + id.remove(0).to_string(); + let seed_id: String = format!("{}@{}", self.data().exchange_contract_id, id); + + let temp = self.data().seed_id_amount.get(&seed_id).unwrap(); + temp + } + + ///Return the total_supply of an specific fft_share (ref lp token). + pub fn total_supply_amount(&self, fft_share: String) -> u128 { + let result = self.data().total_supply_by_fft_share.get(&fft_share); + + if let Some(res) = result { + res + } else { + 0 + } + } + + ///Return the total_supply of an specific fft_share (ref lp token). + pub fn total_supply_by_pool_id(&mut self, token_id: String) -> u128 { + let seed_id: String = format!("{}@{}", self.data_mut().exchange_contract_id, token_id); + log!("Total supply of: {}", seed_id); + let fft_share_id = self + .data_mut() + .fft_share_by_seed_id + .get(&seed_id) + .unwrap() + .clone(); + + let result = self.data_mut().total_supply_by_fft_share.get(&fft_share_id); + if let Some(res) = result { + res + } else { + 0u128 + } + } + pub fn convert_pool_id_in_fft_share(&mut self, token_id: String) -> String { + let seed_id: String = format!("{}@{}", self.data_mut().exchange_contract_id, token_id); + + let fft_share_id = self + .data_mut() + .fft_share_by_seed_id + .get(&seed_id) + .unwrap() + .clone(); + log!("fft id is: {}", fft_share_id); + fft_share_id + } + + ///Assigns a fft_share value to an user for a specific fft_share (ref lp token) + /// and increment the total_supply of this seed's fft_share. + /// It returns the user's new balance. + pub fn mft_mint(&mut self, fft_share: String, balance: u128, user: String) -> u128 { + //Add balance to the user for this seed + let old_amount: u128 = self.users_fft_share_amount(fft_share.clone(), user.clone()); + + let new_balance = old_amount + balance; + log!("{} + {} = new_balance {}", old_amount, balance, new_balance); + + let mut map_temp = self + .data() + .users_balance_by_fft_share + .get(&fft_share) + .expect("err: fft does not exist"); + + map_temp.insert(&user, &new_balance); + + self.data_mut() + .users_balance_by_fft_share + .insert(&fft_share, &map_temp); + + //Add balance to the total supply + let old_total = self.total_supply_amount(fft_share.clone()); + self.data_mut() + .total_supply_by_fft_share + .insert(&fft_share, &(old_total + balance)); + + //Returning the new balance + new_balance + } + + ///Burn fft_share value for an user in a specific fft_share (ref lp token) + /// and decrement the total_supply of this seed's fft_share. + /// It returns the user's new balance. + pub fn mft_burn(&mut self, fft_share: String, balance: u128, user: String) -> u128 { + //Sub balance to the user for this seed + let old_amount: u128 = self.users_fft_share_amount(fft_share.clone(), user.clone()); + assert!(old_amount >= balance); + let new_balance = old_amount - balance; + log!("{} - {} = new_balance {}", old_amount, balance, new_balance); + + let mut map_temp = self + .data() + .users_balance_by_fft_share + .get(&fft_share) + .expect("err: fft does not exist"); + + map_temp.insert(&user, &new_balance); + + self.data_mut() + .users_balance_by_fft_share + .insert(&fft_share, &map_temp); + + //Sub balance to the total supply + let old_total = self.total_supply_amount(fft_share.clone()); + self.data_mut() + .total_supply_by_fft_share + .insert(&fft_share, &(old_total - balance)); + + //Returning the new balance + new_balance + } + + /// Transfer fft_shares internally (user for user). + /// Token_id is a specific fft_share. + #[payable] + pub fn mft_transfer( + &mut self, + token_id: String, + receiver_id: String, + amount: U128, + memo: Option, + ) { + assert_one_yocto(); + log!("{}", env::predecessor_account_id().to_string()); + self.assert_contract_running(); + self.internal_mft_transfer( + token_id, + env::predecessor_account_id().to_string(), + receiver_id, + amount.0, + memo, + ); + } + + fn internal_mft_transfer( + &mut self, + token_id: String, + sender_id: String, + receiver_id: String, + amount: u128, + memo: Option, + ) { + assert_ne!(sender_id, receiver_id, "{}", ERR33_TRANSFER_TO_SELF); + self.share_transfer( + token_id.clone(), + sender_id.clone(), + receiver_id.clone(), + amount, + ); + + log!( + "Transfer shares {}: {} from {} to {}", + token_id, + amount, + sender_id, + receiver_id + ); + + if let Some(memo) = memo { + log!("Memo: {}", memo); + } + } + + pub fn share_transfer( + &mut self, + fft_share: String, + sender_id: String, + receiver_id: String, + amount: u128, + ) { + log!("{} and {}", sender_id, fft_share); + let old_amount: u128 = self.users_fft_share_amount(fft_share.clone(), sender_id.clone()); + log!("{} > = {}", old_amount, amount); + assert!(old_amount >= amount); + log!("{} - {}", old_amount, amount); + let new_balance = old_amount - amount; + log!("{} + {} = new_balance {}", old_amount, amount, new_balance); + + let mut map_temp = self + .data() + .users_balance_by_fft_share + .get(&fft_share) + .expect("err: fft does not exist"); + + map_temp.insert(&sender_id, &new_balance); + + self.data_mut() + .users_balance_by_fft_share + .insert(&fft_share, &map_temp); + + let old_amount: u128 = self.users_fft_share_amount(fft_share.clone(), receiver_id.clone()); + let new_balance = old_amount + amount; + log!("{} + {} = new_balance {}", old_amount, amount, new_balance); + + let mut map_temp = self + .data() + .users_balance_by_fft_share + .get(&fft_share) + .expect("err: fft does not exist"); + + map_temp.insert(&receiver_id, &new_balance); + + self.data_mut() + .users_balance_by_fft_share + .insert(&fft_share, &map_temp); + } + + ///Transfer fft_shares internally (account to account), + /// call mft_on_transfer in the receiver contract and + /// refound something if it is necessary. + #[payable] + pub fn mft_transfer_call( + &mut self, + token_id: String, + receiver_id: AccountId, + amount: U128, + memo: Option, + msg: String, + ) -> PromiseOrValue { + assert_one_yocto(); + self.assert_contract_running(); + let sender_id = env::predecessor_account_id(); + self.internal_mft_transfer( + token_id.clone(), + sender_id.to_string(), + receiver_id.to_string(), + amount.0, + memo, + ); + ext_share_token_receiver::mft_on_transfer( + token_id.clone(), + sender_id.clone(), + amount, + msg, + receiver_id.clone(), + NO_DEPOSIT, + env::prepaid_gas() - GAS_FOR_FT_TRANSFER_CALL, + ) + .then(ext_self::mft_resolve_transfer( + token_id, + sender_id, + receiver_id, + amount, + env::current_account_id(), + NO_DEPOSIT, + GAS_FOR_RESOLVE_TRANSFER, + )) + .into() + } + + /// Returns how much was refunded back to the sender. + /// If sender removed account in the meantime, the tokens are sent to the owner account. + /// Tokens are never burnt. + #[private] + pub fn mft_resolve_transfer( + &mut self, + token_id: String, + sender_id: AccountId, + receiver_id: &AccountId, + amount: U128, + ) -> U128 { + let unused_amount = match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(value) => { + if let Ok(unused_amount) = near_sdk::serde_json::from_slice::(&value) { + std::cmp::min(amount.0, unused_amount.0) + } else { + amount.0 + } + } + PromiseResult::Failed => amount.0, + }; + if unused_amount > 0 { + let receiver_balance = + self.users_fft_share_amount(token_id.clone(), (*receiver_id).to_string()); + if receiver_balance > 0 { + let refund_amount = std::cmp::min(receiver_balance, unused_amount); + // If sender's account was deleted, we assume that they have also withdrew all the liquidity from pools. + // Funds are sent to the owner account. + let refund_to = if self.data().accounts.get(&sender_id).is_some() { + sender_id + } else { + self.data().owner_id.clone() + }; + self.internal_mft_transfer( + token_id, + (*receiver_id).to_string(), + refund_to.to_string(), + refund_amount, + None, + ); + } + } + U128(unused_amount) + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use near_sdk::test_utils::VMContextBuilder; + use near_sdk::{testing_env, VMContext}; + + fn get_context() -> VMContextBuilder { + let mut builder = VMContextBuilder::new(); + builder + .current_account_id(to_account_id("auto_compounder.near")) + .signer_account_id(to_account_id("auto_compounder.near")) + .predecessor_account_id(to_account_id("auto_compounder.near")) + .attached_deposit(1); + builder + } + + pub fn to_account_id(value: &str) -> AccountId { + value.parse().unwrap() + } + + fn create_account() -> Account { + let account_struct = Account::new(&to_account_id("fluxus.near")); + account_struct + } + + #[test] + fn test_mint() { + let context = get_context(); + testing_env!(context.build()); + + let mut account = create_account(); + let mut contract = Contract::new( + "auto_compounder.near".parse().unwrap(), + "ref-finance-101.testnet".parse().unwrap(), + "farm101.fluxusfi.testnet".parse().unwrap(), + "dev-1656420526638-61041719201929".parse().unwrap(), + ); + + //Registering seed + contract.register_seed("fft_share_1".to_string()); + + //Minting fft_share + let mut deposit = + contract.mft_mint("fft_share_1".to_string(), 10_u128, "user1".to_string()); + assert_eq!(deposit, 10_u128); + + //Checking balance + let mut balance = + contract.users_fft_share_amount("fft_share_1".to_string(), "user1".to_string()); + assert_eq!(balance, 10_u128); + + //Minting more fft_share + deposit = contract.mft_mint("fft_share_1".to_string(), 10_u128, "user1".to_string()); + assert_eq!(deposit, 20_u128); + } + + #[test] + fn test_burn() { + let context = get_context(); + testing_env!(context.build()); + + let mut account = create_account(); + let mut contract = Contract::new( + "auto_compounder.near".parse().unwrap(), + "ref-finance-101.testnet".parse().unwrap(), + "farm101.fluxusfi.testnet".parse().unwrap(), + "dev-1656420526638-61041719201929".parse().unwrap(), + ); + + //Seed register + contract.register_seed("fft_share_1".to_string()); + + //Minting fft_share + let mut deposit = + contract.mft_mint("fft_share_1".to_string(), 10_u128, "user1".to_string()); + assert_eq!(deposit, 10_u128); + + //burning fft_share + let mut balance = contract.mft_burn("fft_share_1".to_string(), 2_u128, "user1".to_string()); + assert_eq!(balance, 8_u128); + + //Checking total supply + balance = contract.total_supply_amount("fft_share_1".to_string()); + assert_eq!(balance, 8_u128); + } + + #[test] + fn test_transfer() { + let context = get_context(); + testing_env!(context.build()); + let mut account = create_account(); + let mut contract = Contract::new( + "auto_compounder.near".parse().unwrap(), + "ref-finance-101.testnet".parse().unwrap(), + "farm101.fluxusfi.testnet".parse().unwrap(), + "dev-1656420526638-61041719201929".parse().unwrap(), + ); + + //Seed register + contract.register_seed("fft_share_1".to_string()); + + //Minting balance for users + let mut balance_user1 = contract.mft_mint( + "fft_share_1".to_string(), + 10_u128, + "auto_compounder.near".to_string(), + ); + assert_eq!(balance_user1, 10_u128); + let mut balance_user2 = + contract.mft_mint("fft_share_1".to_string(), 10_u128, "user2".to_string()); + assert_eq!(balance_user2, 10_u128); + let mut balance_user3 = + contract.mft_mint("fft_share_1".to_string(), 999_u128, "user3".to_string()); + assert_eq!(balance_user3, 999_u128); + + //Checking total supply + let total_supply = contract.total_supply_amount("fft_share_1".to_string()); + assert_eq!(total_supply, 1019_u128); + + //Transferring fft_shares + contract.mft_transfer( + "fft_share_1".to_string(), + "user2".to_string(), + U128::from(5_u128), + None, + ); + balance_user1 = contract.users_fft_share_amount( + "fft_share_1".to_string(), + "auto_compounder.near".to_string(), + ); + assert_eq!(balance_user1, 5_u128); + balance_user2 = + contract.users_fft_share_amount("fft_share_1".to_string(), "user2".to_string()); + assert_eq!(balance_user2, 15_u128); + + //Transferring fft_shares + contract.mft_transfer( + "fft_share_1".to_string(), + "user3".to_string(), + U128::from(5_u128), + None, + ); + balance_user1 = contract.users_fft_share_amount( + "fft_share_1".to_string(), + "auto_compounder.near".to_string(), + ); + assert_eq!(balance_user1, 0_u128); + balance_user3 = + contract.users_fft_share_amount("fft_share_1".to_string(), "user3".to_string()); + assert_eq!(balance_user3, 1004_u128); + } +} diff --git a/fluxus-safe/src/owner.rs b/fluxus-safe/src/owner.rs index c2a2b1b..a881696 100644 --- a/fluxus-safe/src/owner.rs +++ b/fluxus-safe/src/owner.rs @@ -21,7 +21,7 @@ impl Contract { pub fn update_treasure_contract(&mut self, contract_id: AccountId) { self.is_owner(); - self.data_mut().treasure_contract_id = contract_id; + self.data_mut().treasury.account_id = contract_id; } /// Returns allowed_accounts @@ -44,25 +44,25 @@ impl Contract { info } + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + /// state: Running, Ended, ... pub fn update_compounder_state( &mut self, - token_id: String, + farm_id_str: String, state: AutoCompounderState, ) -> String { self.is_owner(); - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); - let compounder = strat.get_mut(); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + let compounder_mut = self.get_strat_mut(&token_id.to_string()).get_mut(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); - if compounder.state != state { - compounder.state = state; + if farm_info_mut.state != state { + farm_info_mut.state = state; } - format!("The current state is {:#?}", compounder.state) + format!("The current state is {:#?}", farm_info_mut.state) } /// Extend guardians. Only can be called by owner. @@ -94,22 +94,22 @@ impl Contract { } /// Update slippage for given token_id - pub fn update_strat_slippage(&mut self, token_id: String, new_slippage: u128) -> String { + /// Args: + /// farm_id_str: exchange@pool_id#farm_id + /// new_slippage: value between 80-100 + pub fn update_strat_slippage(&mut self, farm_id_str: String, new_slippage: u128) -> String { assert!(self.is_owner_or_guardians(), "ERR_"); // TODO: what maximum slippage should be accepted? // Should not accept, say, 0 slippage - let strat = self - .data_mut() - .strategies - .get_mut(&token_id) - .expect(ERR21_TOKEN_NOT_REG); + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); - let compounder = strat.get_mut(); - compounder.slippage = 100 - new_slippage; + let compounder_mut = self.get_strat_mut(&token_id.to_string()).get_mut(); + let farm_info_mut = compounder_mut.get_mut_farm_info(farm_id); + farm_info_mut.slippage = 100 - new_slippage; format!( "The current slippage for {} is {}", - token_id, compounder.slippage + token_id, farm_info_mut.slippage ) } } diff --git a/fluxus-safe/src/storage_impl.rs b/fluxus-safe/src/storage_impl.rs index bc10027..09c71dc 100644 --- a/fluxus-safe/src/storage_impl.rs +++ b/fluxus-safe/src/storage_impl.rs @@ -39,15 +39,14 @@ impl StorageManagement for Contract { if already_registered { let amount_already_deposited = self - .data_mut() + .data() .users_total_near_deposited - .get_mut(&account_id.clone()) - .unwrap() - .clone(); + .get(&account_id.clone()) + .unwrap(); self.data_mut() .users_total_near_deposited - .insert(account_id.clone(), amount + amount_already_deposited); + .insert(&account_id, &(amount + amount_already_deposited)); log!( "before + user_deposited_amount = {}", @@ -56,7 +55,7 @@ impl StorageManagement for Contract { } else { self.data_mut() .users_total_near_deposited - .insert(account_id.clone(), amount); + .insert(&account_id, &amount); log!("0 + amount = {}", amount); } self.storage_balance_of(account_id.try_into().unwrap()) @@ -75,11 +74,10 @@ impl StorageManagement for Contract { ); let amount_already_deposited = self - .data_mut() + .data() .users_total_near_deposited - .get_mut(&account_id.clone()) - .unwrap() - .clone(); + .get(&account_id) + .unwrap(); require!( amount_already_deposited >= amount, @@ -103,7 +101,7 @@ impl StorageManagement for Contract { self.data_mut() .users_total_near_deposited - .insert(account_id.clone(), amount_already_deposited - amount); + .insert(&account_id, &(amount_already_deposited - amount)); let withdraw_amount = self.internal_storage_withdraw(&account_id, amount); Promise::new(account_id.clone()).transfer(withdraw_amount); self.storage_balance_of(account_id.try_into().unwrap()) diff --git a/fluxus-safe/src/token_receiver.rs b/fluxus-safe/src/token_receiver.rs index 0930d39..a14f9f3 100644 --- a/fluxus-safe/src/token_receiver.rs +++ b/fluxus-safe/src/token_receiver.rs @@ -81,6 +81,7 @@ pub trait MFTTokenReceiver { #[near_bindgen] impl MFTTokenReceiver for Contract { /// Callback on receiving tokens by this contract. + #[allow(unused)] fn mft_on_transfer( &mut self, token_id: String, @@ -88,10 +89,10 @@ impl MFTTokenReceiver for Contract { amount: U128, msg: String, ) -> PromiseOrValue { - self.assert_strategy_running(token_id.clone()); + self.assert_token_id(token_id.clone()); //Check: Is the token_id the vault's pool_id? If is not, send it back - let strat = self.get_strat(&token_id); + let strat = self.get_strat(token_id.clone()); let compounder = strat.get(); diff --git a/fluxus-safe/src/views.rs b/fluxus-safe/src/views.rs index 59855a4..278c5b0 100644 --- a/fluxus-safe/src/views.rs +++ b/fluxus-safe/src/views.rs @@ -2,26 +2,6 @@ use crate::*; #[near_bindgen] impl Contract { - /// Returns the number of shares some accountId has in the contract - /// Panics if token_id does not exist - pub fn get_user_shares(&self, account_id: AccountId, token_id: String) -> SharesBalance { - let strat = self.get_strat(&token_id); - - let compounder = strat.clone().get(); - - let shares = compounder - .user_shares - .get(&account_id) - .unwrap_or(&SharesBalance { - deposited: 0u128, - total: 0u128, - }) - .clone(); - - log!("{:#?} has {:#?}", account_id.to_string(), shares); - shares - } - /// Function that return the user`s near storage. /// WARN: DEPRECATED pub fn get_user_storage_state(&self, account_id: AccountId) -> Option { @@ -65,7 +45,7 @@ impl Contract { /// Returns the minimum value accepted for given token_id pub fn get_seed_min_deposit(self, token_id: String) -> U128 { - let strat = self.get_strat(&token_id); + let strat = self.get_strat(token_id); let compounder = strat.clone().get(); compounder.seed_min_deposit } @@ -79,49 +59,79 @@ impl Contract { .map(|x| x.to_string()) } - /// Returns all token ids filtering by running strategies + /// Returns all token ids pub fn get_allowed_tokens(&self) -> Vec { + let mut seeds: Vec = Vec::new(); + + for (token_id, _) in self.data().strategies.iter() { + seeds.push(token_id.clone()); + } + + seeds + } + + pub fn get_running_farm_ids(&self) -> Vec { let mut running_strategies: Vec = Vec::new(); for token in self.data().token_ids.clone() { - let strat = self.get_strat(&token); - if strat.get_ref().state == AutoCompounderState::Running { - running_strategies.push(token); + let strat = self.get_strat(token); + let compounder = strat.get_ref(); + for farm in compounder.farms.iter() { + if farm.state == AutoCompounderState::Running { + let farm_id = format!("{}#{}", compounder.seed_id, farm.id); + running_strategies.push(farm_id); + } } } running_strategies } - /// Return all Strategies filtering by running - pub fn get_strats(self) -> Vec { + /// Return all Strategies + pub fn get_strategies(self) -> Vec { let mut info: Vec = Vec::new(); for (token_id, strat) in self.data().strategies.clone() { let compounder = strat.get(); - - info.push(AutoCompounderInfo { - state: compounder.state, + let mut seed_info = AutoCompounderInfo { token_id, - token1_address: compounder.token1_address, - token2_address: compounder.token2_address, - pool_id_token1_reward: compounder.pool_id_token1_reward, - pool_id_token2_reward: compounder.pool_id_token2_reward, - reward_token: compounder.reward_token, - farm_id: compounder.farm_id, - pool_id: compounder.pool_id, - seed_min_deposit: compounder.seed_min_deposit, - seed_id: compounder.seed_id, - }) + is_active: false, + reward_tokens: vec![], + }; + for farm_info in compounder.farms.iter() { + if farm_info.state == AutoCompounderState::Running { + seed_info.is_active = true; + } + seed_info + .reward_tokens + .push(farm_info.reward_token.to_string()); + } + + info.push(seed_info) } info } - pub fn get_strat_state(self, token_id: String) -> AutoCompounderState { - let strat = self.get_strat(&token_id); - let compounder = strat.get(); - compounder.state + pub fn get_strategies_info(&self) -> Vec { + let mut info: Vec = Vec::new(); + for (_, strat) in self.data().strategies.iter() { + for farm in strat.get_ref().farms.iter() { + info.push(farm.clone()); + } + } + + info + } + + pub fn get_strat_state(self, farm_id_str: String) -> AutoCompounderState { + let (seed_id, token_id, farm_id) = get_ids_from_farm(farm_id_str.to_string()); + + let strat = self.get_strat(token_id); + let compounder = strat.get_ref(); + let farm_info = compounder.get_farm_info(&farm_id); + + farm_info.state } /// Returns exchange and farm contracts @@ -137,35 +147,105 @@ impl Contract { self.data().guardians.to_vec() } - /// Returns current amount holden by the contract - pub fn get_contract_amount(self) -> U128 { - let mut amount: u128 = 0; + /// TODO: refactor it + // /// Returns current amount holden by the contract + // pub fn get_contract_amount(self) -> U128 { + // let mut amount: u128 = 0; - for (_, strat) in self.data().strategies.clone() { - let compounder = strat.get(); + // for (_, strat) in self.data().strategies.clone() { + // let compounder = strat.get(); - for (_, shares) in compounder.user_shares { - amount += shares.total; + // for (_, shares) in compounder.user_shares { + // amount += shares.total; + // } + // } + // U128(amount) + // } + + /// TODO: refactor it + ///Return the u128 number of strategies that we have for a specific seed_id. + // pub fn number_of_strategies_by_seed(&self, seed_id: String) -> u128 { + // let num = self.data().compounders_by_seed_id.get(&seed_id); + // let mut result = 0_u128; + // if let Some(number) = num { + // result = (*number).len() as u128; + // } + // result + // } + + /// Return the total number of strategies created, running or others + pub fn number_of_strategies(&self) -> u128 { + let mut count: u128 = 0; + + for (_, strat) in self.data().strategies.iter() { + let size = strat.get_ref().farms.len(); + count += size as u128; + } + + count + } + + pub fn check_fee_by_strategy(&self, token_id: String) -> String { + let compounder = self.get_strat(token_id).get_ref().clone(); + format!("{}%", compounder.admin_fees.strategy_fee) + } + + pub fn is_strategy_active(&self, token_id: String) -> bool { + let compounder = self.get_strat(token_id).get_ref().clone(); + + for farm in compounder.farms.iter() { + if farm.state == AutoCompounderState::Running { + return true; } } - U128(amount) + + false + } + + pub fn current_strat_step(&self, farm_id_str: String) -> String { + let (_, token_id, farm_id) = get_ids_from_farm(farm_id_str); + let compounder = self.get_strat(token_id).get_ref().clone(); + let farm_info = compounder.get_farm_info(&farm_id); + + match farm_info.cycle_stage { + AutoCompounderCycle::ClaimReward => "claim_reward".to_string(), + AutoCompounderCycle::Withdrawal => "withdraw".to_string(), + AutoCompounderCycle::Swap => "swap".to_string(), + AutoCompounderCycle::Stake => "stake".to_string(), + } + } + + pub fn get_farm_ids_by_seed(&self, token_id: String) -> Vec { + let mut strats: Vec = vec![]; + + let compounder = self.get_strat(token_id.clone()).get_ref().clone(); + + for farm in compounder.farms.iter() { + strats.push(format!("{}#{}", token_id, farm.id)); + } + + strats + } + + pub fn get_harvest_timestamp(&self, token_id: String) -> String { + let compounder = self.get_strat(token_id).get_ref().clone(); + compounder.harvest_timestamp.to_string() + } + + pub fn get_strategy_kind(&self) -> String { + match self.data().strategies.values().next() { + Some(x) => x.kind(), + None => "No strategies available".into(), + } } } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(crate = "near_sdk::serde")] pub struct AutoCompounderInfo { - pub state: AutoCompounderState, pub token_id: String, - pub token1_address: AccountId, - pub token2_address: AccountId, - pub pool_id_token1_reward: u64, - pub pool_id_token2_reward: u64, - pub reward_token: AccountId, - pub farm_id: String, - pub pool_id: u64, - pub seed_min_deposit: U128, - pub seed_id: String, + pub is_active: bool, + pub reward_tokens: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/fluxus-safe/tests/general.rs b/fluxus-safe/tests/general.rs index c87beb1..78e366c 100644 --- a/fluxus-safe/tests/general.rs +++ b/fluxus-safe/tests/general.rs @@ -1,78 +1,232 @@ +use std::collections::HashMap; + +use serde_json::json; + +use fluxus_safe::{self, get_ids_from_farm}; mod utils; +use near_sdk::json_types::U128; use near_units::parse_near; +use percentage::Percentage; use workspaces::{ - network::{DevAccountDeployer, TopLevelAccountCreator}, - operations::Transaction, + network::{DevAccountDeployer, Sandbox}, Account, AccountId, Contract, Network, Worker, }; -const TOTAL_GAS: u64 = 300_000_000_000_000; +async fn fast_forward( + blocks_to_forward: u64, + fast_forward_counter: &mut u64, + worker: &Worker, +) -> anyhow::Result { + let block_info = worker.view_latest_block().await?; + println!( + "BlockInfo {fast_forward_counter} pre-fast_forward {:?}", + block_info + ); -const CONTRACT_ID_REF_EXC: &str = "exchange.ref-dev.testnet"; + worker.fast_forward(blocks_to_forward).await?; -/// Runs the full cycle of auto-compound -async fn do_auto_compound( - contract: &Contract, - farm: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let res = contract - .call(&worker, "claim_reward") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) - .transact() - .await?; - // println!("claim_reward {:#?}\n", res); + let block_info = worker.view_latest_block().await?; + println!( + "BlockInfo {fast_forward_counter} post-fast_forward {:?}", + block_info + ); - let res = contract - .call(&worker, "withdraw_of_reward") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) - .transact() - .await?; - // println!("withdraw_of_reward {:#?}\n", res); + *fast_forward_counter += 1; - let res = contract - .call(&worker, "autocompounds_swap") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) - .transact() - .await?; - // println!("autocompounds_swap {:#?}\n", res); + Ok(0) +} - let res = contract - .call(&worker, "autocompounds_liquidity_and_stake") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) - .transact() - .await?; - // println!("autocompounds_liquidity_and_stake {:#?}\n", res); +async fn do_harvest( + sentry_acc: &Account, + safe_contract: &Contract, + farm_id_str: &String, + worker: &Worker, +) -> anyhow::Result { + //Check amount of unclaimed rewards the Strategy has + let mut unclaimed_amount = 0u128; + + for i in 0..4 { + let _res = sentry_acc + .call(worker, safe_contract.id(), "harvest") + .args_json(serde_json::json!({ "farm_id_str": farm_id_str }))? + .gas(utils::TOTAL_GAS) + .transact() + .await?; + println!("harvest step {}: {:#?}\n", i + 1, _res); + + if i == 0 { + unclaimed_amount = _res.json()?; + } + } + + Ok(unclaimed_amount) +} + +/// Runs the full cycle of auto-compound and fast forward +async fn do_auto_compound_with_fast_forward( + sentry_acc: &Account, + contract: &Contract, + farm_id_str: &String, + blocks_to_forward: u64, + fast_forward_counter: &mut u64, + worker: &Worker, +) -> anyhow::Result { + if blocks_to_forward > 0 { + fast_forward(blocks_to_forward, fast_forward_counter, worker).await?; + } - // utils::log_farm_seeds(&contract, &farm, &worker).await?; + let unclaimed_amount = do_harvest(sentry_acc, contract, farm_id_str, worker).await?; - Ok(()) + Ok(unclaimed_amount) } -/// Return the number of shares that the account has in the auto-compound contract +/// Return the number of shares that the account has in the auto-compound contract for given seed async fn get_user_shares( contract: &Contract, + account_id: &AccountId, + seed_id: &String, + worker: &Worker, +) -> anyhow::Result { + let args = json!({ + "seed_id": seed_id, + "user": account_id.to_string(), + }) + .to_string() + .into_bytes(); + + let account_shares = contract + .view(worker, "user_share_seed_id", args) + .await? + .json()?; + + Ok(account_shares) +} + +/// Create new account, register into exchange and deposit into exchange +async fn create_ready_account( + owner: &Account, + exchange: &Contract, + token_1: &Contract, + token_2: &Contract, + worker: &Worker, +) -> anyhow::Result { + let new_account = worker.dev_create_account().await?; + // Transfer from owner to multiple accounts + utils::transfer_tokens( + owner, + vec![&new_account], + maplit::hashmap! { + token_1.id() => parse_near!("1,000 N"), + token_2.id() => parse_near!("1,000 N"), + }, + worker, + ) + .await?; + // register accounts into exchange and transfer tokens + utils::register_into_contracts(worker, &new_account, vec![exchange.id()]).await?; + utils::deposit_tokens( + worker, + &new_account, + exchange, + maplit::hashmap! { + token_1.id() => parse_near!("30 N"), + token_2.id() => parse_near!("30 N"), + }, + ) + .await?; + + Ok(new_account) +} + +/// Adds liquidity to the pool and stake received shares into safe +async fn stake_into_safe( + safe_contract: &Contract, + exchange: &Contract, account: &Account, + pool_id: u64, + seed_id1: &String, worker: &Worker, ) -> anyhow::Result { - let res = contract - .call(&worker, "get_user_shares") + // add liquidity to pool + let _res = account + .call(worker, exchange.id(), "add_liquidity") .args_json(serde_json::json!({ - "account_id": account.id() + "pool_id": pool_id.clone(), + "amounts": vec![parse_near!("20 N").to_string(), parse_near!("20 N").to_string()], + }))? + .deposit(parse_near!("1 N")) + .transact() + .await?; + + // Get account 1 shares + let initial_shares: String = utils::get_pool_shares(account, exchange, pool_id, worker).await?; + + let token_id: String = format!(":{}", pool_id); + + /* Stake */ + let _res = account + .call(worker, exchange.id(), "mft_transfer_call") + .args_json(serde_json::json!({ + "token_id": token_id.clone(), + "receiver_id": safe_contract.id().to_string(), + "amount": initial_shares, + "msg": "" }))? - .gas(TOTAL_GAS) + .gas(utils::TOTAL_GAS) + .deposit(parse_near!("1 yN")) .transact() .await?; + // println!("mft_transfer_call {:#?}\n", res);// + + let deposited_shares = get_user_shares(safe_contract, account.id(), seed_id1, worker).await?; - let account_shares: String = res.json()?; - let shares: u128 = utils::str_to_u128(&account_shares); + // assert that contract received the correct number of shares, due to precision issues derived of recurring tithe we must not accept aboslute errors bigger than 9 + // TODO: validates with fuzzing and way more testing to ensure absolute error is not bigger than 9 + let account1_shares_as_int = i128::try_from(deposited_shares).unwrap(); + assert!( + (account1_shares_as_int - utils::str_to_i128(&initial_shares)).abs() < 9, + "ERR: the amount of shares doesn't match there is : {} should be {}", + deposited_shares, + initial_shares + ); - Ok(shares) + Ok(deposited_shares) +} + +async fn deploy_aux_contracts( + owner: &Account, + exchange_id: &AccountId, + worker: &Worker, +) -> (Contract, Contract, Contract, Contract, Contract, Contract) { + let token_1 = (utils::create_custom_ft(owner, worker).await).unwrap(); + let token_2 = (utils::create_custom_ft(owner, worker).await).unwrap(); + let token_reward_1 = (utils::create_custom_ft(owner, worker).await).unwrap(); + let token_reward_2 = (utils::create_custom_ft(owner, worker).await).unwrap(); + + let exchange = (utils::deploy_exchange( + owner, + exchange_id, + vec![ + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], + worker, + ) + .await) + .unwrap(); + + let treasury = (utils::deploy_treasure(owner, &token_1, worker).await).unwrap(); + ( + token_1, + token_2, + token_reward_1, + token_reward_2, + exchange, + treasury, + ) } #[tokio::test] @@ -80,29 +234,42 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; let owner = worker.root_account(); - let exchange_id: AccountId = CONTRACT_ID_REF_EXC.parse().unwrap(); + let exchange_id: AccountId = utils::CONTRACT_ID_REF_EXC.parse().unwrap(); /////////////////////////////////////////////////////////////////////////// // Stage 1: Deploy relevant contracts /////////////////////////////////////////////////////////////////////////// - let token_1 = utils::create_custom_ft(&owner, &worker).await?; - let token_2 = utils::create_custom_ft(&owner, &worker).await?; - let token_reward = utils::create_custom_ft(&owner, &worker).await?; - let exchange = utils::deploy_exchange( - &owner, - &exchange_id, - vec![&token_1.id(), &token_2.id(), &token_reward.id()], - &worker, - ) - .await?; + let (token_1, token_2, token_reward_1, token_reward_2, exchange, treasury) = + deploy_aux_contracts(&owner, &exchange_id, &worker).await; + + println!( + "token1 {} token2 {} reward1 {} reward2 {} exchange {} treasury {}", + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + exchange.id(), + treasury.id(), + ); - // Transfer tokens from owner to new account - let account_1 = worker.dev_create_account().await?; + // Create multiple accounts + let farmer1 = worker.dev_create_account().await?; + let strat_creator_acc = worker.dev_create_account().await?; + let sentry_acc = worker.dev_create_account().await?; + + println!( + "Ids: owner {} farmer1 {} strat_creator_acc {} sentry_acc {}", + owner.id(), + farmer1.id(), + strat_creator_acc.id(), + sentry_acc.id() + ); + // Transfer from owner to multiple accounts utils::transfer_tokens( &owner, - &account_1, + vec![&farmer1, &strat_creator_acc, treasury.as_account()], maplit::hashmap! { token_1.id() => parse_near!("10,000 N"), token_2.id() => parse_near!("10,000 N"), @@ -115,16 +282,62 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { utils::register_into_contracts( &worker, exchange.as_account(), - vec![token_1.id(), token_2.id(), token_reward.id()], + vec![ + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], ) .await?; - // register account 1 into exchange and transfer tokens - utils::register_into_contracts(&worker, &account_1, vec![exchange.id()]).await?; + // Register Sentry into tokens + utils::register_into_contracts( + &worker, + &sentry_acc, + vec![ + exchange.id(), + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], + ) + .await?; + + // Register Strat creator into tokens + utils::register_into_contracts( + &worker, + &strat_creator_acc, + vec![ + exchange.id(), + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], + ) + .await?; + + utils::register_into_contracts( + &worker, + treasury.as_account(), + vec![ + exchange.id(), + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], + ) + .await?; + + // register accounts into exchange and transfer tokens + utils::register_into_contracts(&worker, &farmer1, vec![exchange.id()]).await?; utils::deposit_tokens( &worker, - &account_1, + &farmer1, &exchange, maplit::hashmap! { token_1.id() => parse_near!("30 N"), @@ -137,46 +350,75 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { // Stage 2: Create pools and farm /////////////////////////////////////////////////////////////////////////// - let (pool_token1_token2, pool_token1_reward, pool_token2_reward) = utils::create_pools( + let reward_liquidity = parse_near!("0.000000000000001 N"); + let base_liquidity = parse_near!("1 N"); + + let ( + pool_token1_token2, + pool_token1_reward1, + pool_token2_reward1, + pool_token1_reward2, + pool_token2_reward2, + ) = utils::create_pools( &owner, &exchange, &token_1, &token_2, - &token_reward, + &token_reward_1, + &token_reward_2, + reward_liquidity, + base_liquidity, &worker, ) .await?; - let seed_id: String = format! {"{}@{}", CONTRACT_ID_REF_EXC, pool_token1_token2}; + let seed_id1: String = format! {"{}@{}", utils::CONTRACT_ID_REF_EXC, pool_token1_token2}; + + // Create farms + let farm = utils::deploy_farm(&owner, &worker).await?; + println!("farm contract: {}", farm.id()); + let (farm_str0, farm_id0) = + utils::create_farm(&owner, &farm, &seed_id1, &token_reward_1, true, &worker).await?; - // Create farm - let farm = utils::deploy_farm(&owner, &seed_id, &token_reward, &worker).await?; + println!("Created simple farm!"); /////////////////////////////////////////////////////////////////////////// - // Stage 3: Deploy Vault contract + // Stage 3: Deploy Safe contract /////////////////////////////////////////////////////////////////////////// - let contract = utils::deploy_full_vault_contract( + let safe_contract = utils::deploy_safe_contract(&strat_creator_acc, &treasury, &worker).await?; + println!("safe contract {}", safe_contract.id()); + + utils::create_strategy( + &strat_creator_acc, + &safe_contract, &token_1, &token_2, - &token_reward, - pool_token1_reward, - pool_token2_reward, pool_token1_token2, - 0, + &worker, + ) + .await?; + + utils::add_strategy( + &safe_contract, + &token_reward_1, + pool_token1_reward1, + pool_token2_reward1, + pool_token1_token2, + farm_id0, &worker, ) .await?; /////////////////////////////////////////////////////////////////////////// - // Stage 4: Initialize Vault + // Stage 4: Initialize Safe /////////////////////////////////////////////////////////////////////////// - /* Register vault into farm contract */ - let res = contract + /* Register into farm contract */ + let _res = safe_contract .as_account() .call(&worker, farm.id(), "storage_deposit") - .args_json(serde_json::json!({ "account_id": contract.id() }))? + .args_json(serde_json::json!({ "account_id": safe_contract.id() }))? .deposit(parse_near!("1 N")) .transact() .await?; @@ -184,94 +426,198 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { /* Register contract into tokens */ utils::register_into_contracts( &worker, - contract.as_account(), - vec![&exchange_id, token_1.id(), token_2.id(), token_reward.id()], + safe_contract.as_account(), + vec![ + &exchange_id, + token_1.id(), + token_2.id(), + token_reward_1.id(), + token_reward_2.id(), + ], ) .await?; - let pool_id: String = format!(":{}", pool_token1_token2); + let token_id: String = format!(":{}", pool_token1_token2); - let res = contract + let res = safe_contract .as_account() .call(&worker, exchange.id(), "mft_register") .args_json(serde_json::json!({ - "token_id": pool_id.clone(), - "account_id": contract.id() }))? + "token_id": token_id.clone(), + "account_id": safe_contract.id() }))? .deposit(parse_near!("1 N")) .transact() .await?; // println!("mft_register {:#?}", res); /////////////////////////////////////////////////////////////////////////// - // Stage 5: Start interacting with Vault + // Stage 5: Start interacting with Safe /////////////////////////////////////////////////////////////////////////// let initial_owner_shares: String = utils::get_pool_shares(&owner, &exchange, pool_token1_token2, &worker).await?; - let pool_id: String = format!(":{}", pool_token1_token2); + let fft_token: String = + utils::get_fft_token_by_seed(&safe_contract, &seed_id1, &worker).await?; /* Stake */ let res = owner .call(&worker, exchange.id(), "mft_transfer_call") .args_json(serde_json::json!({ - "token_id": pool_id, - "receiver_id": contract.id().to_string(), + "token_id": token_id, + "receiver_id": safe_contract.id().to_string(), "amount": initial_owner_shares.clone(), "msg": "" }))? - .gas(TOTAL_GAS) + .gas(utils::TOTAL_GAS) .deposit(parse_near!("1 yN")) .transact() .await?; // println!("mft_transfer_call {:#?}\n", res); - let owner_shares_on_contract = get_user_shares(&contract, &owner, &worker).await?; - + let owner_shares_on_contract = + get_user_shares(&safe_contract, &owner.id(), &seed_id1, &worker).await?; // assert that contract received the correct number of shares assert_eq!( owner_shares_on_contract, utils::str_to_u128(&initial_owner_shares), - "ERR" + "ERR: the amount of shares doesn't match there is : {} should be {}", + owner_shares_on_contract, + initial_owner_shares + ); + + let owner_fft_shares = utils::get_user_fft(&safe_contract, &owner, &fft_token, &worker).await?; + assert_eq!( + owner_fft_shares, owner_shares_on_contract, + "ERR: First user should receive the same amount of fft as seed_id1 deposited" ); /////////////////////////////////////////////////////////////////////////// - // Stage 6: Fast forward in the future + // Stage 6: Fast forward in the future and auto-compound /////////////////////////////////////////////////////////////////////////// - let block_info = worker.view_latest_block().await?; - println!("BlockInfo pre-fast_forward {:?}", block_info); - - worker.fast_forward(700).await?; + let mut fast_forward_counter: u64 = 0; + println!("Checking fees balance"); - let block_info = worker.view_latest_block().await?; - println!("BlockInfo post-fast_forward {:?}", block_info); - - // utils::log_farm_info(&farm, &seed_id, &worker).await?; - - /////////////////////////////////////////////////////////////////////////// - // Stage 7: Auto-compound calls - /////////////////////////////////////////////////////////////////////////// + let balance_before_sentry = i128::try_from( + utils::get_balance_of(&sentry_acc, &token_reward_1, true, &worker, None) + .await? + .0, + ) + .unwrap(); + + let balance_before_treasury = i128::try_from( + utils::get_balance_of( + treasury.as_account(), + &exchange, + false, + &worker, + Some(token_reward_1.id().to_string()), + ) + .await? + .0, + ) + .unwrap(); - do_auto_compound(&contract, &farm, &worker).await?; + let balance_before_strat_creator = i128::try_from( + utils::get_balance_of(&strat_creator_acc, &token_reward_1, true, &worker, None) + .await? + .0, + ) + .unwrap(); + + let amount_claimed = do_auto_compound_with_fast_forward( + &sentry_acc, + &safe_contract, + &farm_str0, + 700, + &mut fast_forward_counter, + &worker, + ) + .await?; let owner_deposited_shares: u128 = utils::str_to_u128(&initial_owner_shares); // Get owner shares from auto-compound contract - let round1_owner_shares: u128 = get_user_shares(&contract, &owner, &worker).await?; - + let round1_owner_shares: u128 = + get_user_shares(&safe_contract, owner.id(), &seed_id1, &worker).await?; // Assert the current value is higher than the initial value deposited assert!( round1_owner_shares > owner_deposited_shares, - "ERR_AUTO_COMPOUND_DOES_NOT_WORK" + "ERR_AUTO_COMPOUND_DOES_NOT_WORK. Expected {} and received {}", + owner_deposited_shares, + round1_owner_shares + ); + + let all_fees_amount = Percentage::from(utils::TOTAL_PROTOCOL_FEE).apply_to(amount_claimed); + + println!("Amount claimed: {}", amount_claimed); + + let sentry_due_fees = + i128::try_from(Percentage::from(utils::SENTRY_FEES_PERCENT).apply_to(all_fees_amount)) + .unwrap(); + let strat_creator_due_fees = i128::try_from( + Percentage::from(utils::STRAT_CREATOR_FEES_PERCENT).apply_to(all_fees_amount), + ) + .unwrap(); + let treasury_due_fees = + i128::try_from(Percentage::from(utils::TREASURY_FEES_PERCENT).apply_to(all_fees_amount)) + .unwrap(); + let balance_after_sentry = i128::try_from( + utils::get_balance_of(&sentry_acc, &token_reward_1, true, &worker, None) + .await? + .0, + ) + .unwrap(); + let balance_after_treasury = i128::try_from( + utils::get_balance_of( + treasury.as_account(), + &exchange, + false, + &worker, + Some(token_reward_1.id().to_string()), + ) + .await? + .0, + ) + .unwrap(); + let balance_after_strat_creator = i128::try_from( + utils::get_balance_of(&strat_creator_acc, &token_reward_1, true, &worker, None) + .await? + .0, + ) + .unwrap(); + assert!( + (balance_after_sentry - (sentry_due_fees + balance_before_sentry)).abs() < 9, + "ERR: Sentry did not receive his due fees. there is: {} should be: {}", + balance_after_sentry, + (sentry_due_fees + balance_before_sentry) ); + assert!( + (balance_after_strat_creator - (strat_creator_due_fees + balance_before_strat_creator)) + .abs() + < 9, + "ERR: Strat Creator did not receive his due fees. there is: {} should be: {}", + balance_before_strat_creator, + (strat_creator_due_fees + balance_before_strat_creator) + ); + + assert!( + (balance_after_treasury - (treasury_due_fees + balance_before_treasury)).abs() < 9, + "ERR: Treasury did not receive his due fees. there is: {} should be: {}", + balance_after_treasury, + (strat_creator_due_fees + balance_before_strat_creator) + ); + + println!("First round of auto-compound succeeded! Fees distributed correctly!"); + /////////////////////////////////////////////////////////////////////////// - // Stage 8: Stake with another account + // Stage 7: Stake with another account /////////////////////////////////////////////////////////////////////////// // add liquidity for account 1 - let res = account_1 + let res = farmer1 .call(&worker, exchange.id(), "add_liquidity") .args_json(serde_json::json!({ "pool_id": pool_token1_token2.clone(), @@ -285,92 +631,202 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { // Get account 1 shares let account1_initial_shares: String = - utils::get_pool_shares(&account_1, &exchange, pool_token1_token2.clone(), &worker).await?; + utils::get_pool_shares(&farmer1, &exchange, pool_token1_token2, &worker).await?; let pool_id: String = format!(":{}", pool_token1_token2); /* Stake */ - let res = account_1 + let res = farmer1 .call(&worker, exchange.id(), "mft_transfer_call") .args_json(serde_json::json!({ "token_id": pool_id.clone(), - "receiver_id": contract.id().to_string(), + "receiver_id": safe_contract.id().to_string(), "amount": account1_initial_shares, "msg": "" }))? - .gas(TOTAL_GAS) + .gas(utils::TOTAL_GAS) .deposit(parse_near!("1 yN")) .transact() .await?; - // println!("mft_transfer_call {:#?}\n", res); + // println!("mft_transfer_call {:#?}\n", res);// - let account1_shares_on_contract = get_user_shares(&contract, &account_1, &worker).await?; + let account1_shares_on_contract = + get_user_shares(&safe_contract, &farmer1.id(), &seed_id1, &worker).await?; - // assert that contract received the correct number of shares - assert_eq!( + // assert that contract received the correct number of shares, due to precision issues derived of recurring tithe we must not accept aboslute errors bigger than 9 + // TODO: validates with fuzzing and way more testing to ensure absolute error is not bigger than 9 + let account1_shares_as_int = i128::try_from(account1_shares_on_contract).unwrap(); + assert!( + (account1_shares_as_int - utils::str_to_i128(&account1_initial_shares)).abs() < 9, + "ERR: the amount of shares doesn't match there is : {} should be {}", account1_shares_on_contract, - utils::str_to_u128(&account1_initial_shares), - "ERR" + account1_initial_shares ); + println!("Stage 7 succeeded!"); /////////////////////////////////////////////////////////////////////////// - // Stage 9: Fast forward in the future + // Stage 8: Fast forward in the future and auto-compound /////////////////////////////////////////////////////////////////////////// - let block_info = worker.view_latest_block().await?; - println!("BlockInfo pre-fast_forward {:?}", block_info); - - worker.fast_forward(900).await?; + // utils::log_farm_info(&farm, &seed_id1, &worker).await; - let block_info = worker.view_latest_block().await?; - println!("BlockInfo post-fast_forward {:?}", block_info); - - /////////////////////////////////////////////////////////////////////////// - // Stage 10: Run another round of auto-compound - /////////////////////////////////////////////////////////////////////////// + do_auto_compound_with_fast_forward( + &sentry_acc, + &safe_contract, + &farm_str0, + 900, + &mut fast_forward_counter, + &worker, + ) + .await?; - do_auto_compound(&contract, &farm, &worker).await?; + println!("Stage 8 succeeded!"); /////////////////////////////////////////////////////////////////////////// - // Stage 11: Assert owner and account_1 earned shares from auto-compounder strategy + // Stage 9: Assert owner and farmer1 earned shares from auto-compounder strategy /////////////////////////////////////////////////////////////////////////// // owner shares - let round2_owner_shares: u128 = get_user_shares(&contract, &owner, &worker).await?; + let round2_owner_shares: u128 = + get_user_shares(&safe_contract, owner.id(), &seed_id1, &worker).await?; assert!( round2_owner_shares > round1_owner_shares, - "ERR_AUTO_COMPOUND_DOES_NOT_WORK" + "ERR_AUTO_COMPOUND_DOES_NOT_WORK. Expected {} and received {}", + round1_owner_shares, + round2_owner_shares ); // get account 1 shares from auto-compounder contract - let round2_account1_shares: u128 = get_user_shares(&contract, &account_1, &worker).await?; + let round2_account1_shares: u128 = + get_user_shares(&safe_contract, farmer1.id(), &seed_id1, &worker).await?; // parse String to u128 let account1_initial_shares: u128 = utils::str_to_u128(&account1_initial_shares); assert!( round2_account1_shares > account1_initial_shares, - "ERR_AUTO_COMPOUND_DOES_NOT_WORK" + "ERR_AUTO_COMPOUND_DOES_NOT_WORK. Expected {} and received {}", + account1_initial_shares, + round2_account1_shares ); + println!("Stage 9 succeeded!"); + /////////////////////////////////////////////////////////////////////////// - // Stage 12: Withdraw from Vault and assert received shares are correct + // Stage 10: Withdraw from Safe and assert received shares are correct /////////////////////////////////////////////////////////////////////////// + //Total seed in the contract + let seed_before_withdraw: u128 = + utils::get_seed_total_amount(&safe_contract, &token_id, &worker).await?; + println!( + "Total seed before the withdraw is = {}", + seed_before_withdraw + ); + + //Total user's amount of fft_shares + //Available to unstake + let unstaked: u128 = get_user_shares(&safe_contract, owner.id(), &seed_id1, &worker).await?; + //Calling unstake let res = owner - .call(&worker, contract.id(), "unstake") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) + .call(&worker, safe_contract.id(), "unstake") + .args_json(serde_json::json!({ "token_id": token_id }))? + .gas(utils::TOTAL_GAS) .transact() .await?; - let res = account_1 - .call(&worker, contract.id(), "unstake") - .args_json(serde_json::json!({}))? - .gas(TOTAL_GAS) + //Getting user's amount of shares + let user_amount_of_seed: u128 = + get_user_shares(&safe_contract, owner.id(), &seed_id1, &worker).await?; + + //Checking that the user has no balance after unstake all available. + assert_eq!( + user_amount_of_seed, 0_u128, + "User amount need to be 0 after unstake, but it is {}.", + user_amount_of_seed + ); + + //Getting the total seed available in the contract + let seed_after_withdraw: u128 = + utils::get_seed_total_amount(&safe_contract, &token_id, &worker).await?; + println!("Total seed is = {}", seed_after_withdraw); + + //Checking if the new amount of seed is correct + assert_eq!( + seed_after_withdraw, + seed_before_withdraw - unstaked, + "New amount of seed is incorrect: {} =! {} - {}", + seed_after_withdraw, + seed_before_withdraw, + unstaked + ); + + //Getting the seed amount available for the user farmer1 + let unstaked: u128 = get_user_shares(&safe_contract, farmer1.id(), &seed_id1, &worker).await?; + let mut seed_total = unstaked; + let withdraw1: U128 = U128::from(unstaked / 2_u128); + + //Calling unstake with a half of the user's total available seed + let res = farmer1 + .call(&worker, safe_contract.id(), "unstake") + .args_json(serde_json::json!({ "token_id": token_id , "amount_withdrawal":withdraw1 }))? + .gas(utils::TOTAL_GAS) .transact() .await?; + println!("farmer1 unstaked successfully {:#?}", res); + + //New amount in the contract needs to be: + seed_total -= withdraw1.0; + + let user_amount_of_seed: u128 = + get_user_shares(&safe_contract, farmer1.id(), &seed_id1, &worker).await?; + + //Checking if the user amount of shares + assert!( + user_amount_of_seed - (withdraw1.0) < 10, + "User amount need to be {} after unstake, but it is {}.", + (withdraw1.0), + user_amount_of_seed + ); + + let withdraw2: U128 = U128(user_amount_of_seed); + + //Calling unstake to withdraw the rest + let res = farmer1 + .call(&worker, safe_contract.id(), "unstake") + .args_json(serde_json::json!({ "token_id": token_id , "amount_withdrawal": withdraw2}))? + .gas(utils::TOTAL_GAS) + .transact() + .await?; + println!( + "farmer1 unstaked successfully for the second time{:#?}", + res + ); + + let seed_after_withdraw: u128 = + utils::get_seed_total_amount(&safe_contract, &token_id, &worker).await?; + + //Testing the contract total amount after unstake + assert_eq!( + seed_after_withdraw, + seed_total - user_amount_of_seed, + "New amount of seed is incorrect: {} =! {} - {}", + seed_after_withdraw, + seed_total, + unstaked + ); + + //Getting the user's amount of seed + let user_amount_of_seed: u128 = + get_user_shares(&safe_contract, farmer1.id(), &seed_id1, &worker).await?; + + //Testing the user new amount after unstake + assert_eq!( + user_amount_of_seed, 0_u128, + "User amount need to be 0 after unstake, but it is {}.", + user_amount_of_seed + ); // Get owner shares from exchange let owner_shares_on_exchange: String = @@ -385,14 +841,165 @@ async fn simulate_stake_and_withdraw() -> anyhow::Result<()> { // Get account 1 shares from exchange let account1_shares_on_exchange: String = - utils::get_pool_shares(&account_1, &exchange, pool_token1_token2, &worker).await?; + utils::get_pool_shares(&farmer1, &exchange, pool_token1_token2, &worker).await?; let account1_shares_on_exchange: u128 = utils::str_to_u128(&account1_shares_on_exchange); - assert_eq!( - round2_account1_shares, account1_shares_on_exchange, - "ERR_COULD_NOT_WITHDRAW" + // assert that contract received the correct number of shares, due to precision issues derived of recurring tithe we must not accept aboslute errors bigger than 9 + // TODO: validates with fuzzing and way more testing to ensure absolute error is not bigger than 9 + let round2_account1_shares_as_int = i128::try_from(round2_account1_shares).unwrap(); + let account1_shares_on_exchange_as_int = i128::try_from(account1_shares_on_exchange).unwrap(); + assert!( + (round2_account1_shares_as_int - account1_shares_on_exchange_as_int).abs() < 9, + "ERR: the amount of shares doesn't match. There is {} should be {}", + account1_shares_on_contract, + account1_initial_shares ); + // assert that fft supply is 0 + let seed_total_amount: u128 = + utils::get_seed_total_amount(&safe_contract, &token_id, &worker).await?; + + assert!( + seed_total_amount == 0u128, + "ERR: After withdraw from all users, the supply should be 0" + ); + + println!("Unstake was successful!"); + + /////////////////////////////////////////////////////////////////////////// + // Stage 11: Create another strategy and + // after each round of auto-compound, create another account and stake into safe + /////////////////////////////////////////////////////////////////////////// + + // create another farm with different reward token from the same seed + let (farm_str1, farm_id1) = + utils::create_farm(&owner, &farm, &seed_id1, &token_reward_2, false, &worker).await?; + + // common tokens 1 + // create farm with (token1, token2) pair and token1 as reward + let (farm_str2, farm_id2) = + utils::create_farm(&owner, &farm, &seed_id1, &token_1, false, &worker).await?; + + // common tokens 2 + // create farm with (token1, token2) pair and token2 as reward + let (farm_str3, farm_id3) = + utils::create_farm(&owner, &farm, &seed_id1, &token_2, false, &worker).await?; + + // create farms map to iterate over + let mut farms: HashMap = HashMap::new(); + farms.insert(farm_str0, farm_id0); + farms.insert(farm_str1, farm_id1); + farms.insert(farm_str2, farm_id2); + farms.insert(farm_str3, farm_id3); + + // Adds new strategy to safe + utils::add_strategy( + &safe_contract, + &token_reward_2, + pool_token1_reward2, + pool_token2_reward2, + pool_token1_token2, + farm_id1, + &worker, + ) + .await?; + + // (token1, token2) -> token1 + utils::add_strategy( + &safe_contract, + &token_1, + utils::POOL_ID_PLACEHOLDER, + pool_token1_token2, + pool_token1_token2, + farm_id2, + &worker, + ) + .await?; + + // (token1, token2) -> token2 + utils::add_strategy( + &safe_contract, + &token_2, + pool_token1_token2, + utils::POOL_ID_PLACEHOLDER, + pool_token1_token2, + farm_id3, + &worker, + ) + .await?; + + let mut farmers_map: HashMap = HashMap::new(); + + let blocks_to_forward = 300; + + println!("Starting harvest test with multiple strategies"); + for i in 0..2u64 { + println!("Starting harvest test round {}", i); + + // creates new farmer + let new_farmer = + create_ready_account(&owner, &exchange, &token_1, &token_2, &worker).await?; + // stake into safe + let staked_shares = stake_into_safe( + &safe_contract, + &exchange, + &new_farmer, + pool_token1_token2, + &seed_id1, + &worker, + ) + .await?; + + // store farmer and initial shares + farmers_map.insert(new_farmer.id().clone(), staked_shares); + + println!( + "Created new farmer {} with {} shares", + new_farmer.id(), + staked_shares + ); + + fast_forward(blocks_to_forward, &mut fast_forward_counter, &worker).await?; + + // auto-compound from seed1, farmX + for (farm_str, _) in farms.iter() { + // utils::log_farm_info(&farm, &seed_id1, &worker).await; + + println!("farm_str {}", farm_str); + + do_harvest(&sentry_acc, &safe_contract, farm_str, &worker).await?; + + // checks farmers earnings + for mut farmer in farmers_map.iter_mut() { + let farmer_id = farmer.0; + let current_shares = farmer.1; + + let mut latest_shares: u128 = + get_user_shares(&safe_contract, farmer_id, &seed_id1, &worker).await?; + + assert!( + latest_shares > *current_shares, + "Harvest failed. In loop {} with farm {} from account {} expected {} to be greater than {}", + i, + farm_str, + farmer_id, + latest_shares, + current_shares + ); + + println!( + "{} had {} and now has {}", + farmer_id, current_shares, latest_shares + ); + + // update shares + farmer.1 = &mut latest_shares; + } + } + + println!("Harvest test round {} succeed", i); + } + Ok(()) } diff --git a/fluxus-safe/tests/utils.rs b/fluxus-safe/tests/utils.rs index 741c0af..ff353ff 100644 --- a/fluxus-safe/tests/utils.rs +++ b/fluxus-safe/tests/utils.rs @@ -1,18 +1,30 @@ +use anyhow::Ok; use near_sdk::json_types::U128; use near_units::{parse_gas, parse_near}; use std::collections::HashMap; use tokio::fs; use workspaces::network::Sandbox; use workspaces::prelude::*; -use workspaces::{prelude::*, testnet, DevNetwork}; -use workspaces::{Account, AccountId, Contract, Network, Worker}; +use workspaces::{Account, AccountId, Contract, DevNetwork, Network, Worker}; pub const TOTAL_GAS: u64 = 300_000_000_000_000; pub const MIN_SEED_DEPOSIT: u128 = 1_000_000_000_000; +pub const CONTRACT_ID_REF_EXC: &str = "ref-finance-101.testnet"; +pub const CONTRACT_ID_FARM: &str = "boostfarm.ref-finance.testnet"; +pub const FT_CONTRACT_FILEPATH: &str = "./res/fungible_token.wasm"; + +pub const TOTAL_PROTOCOL_FEE: u128 = 10; +pub const SENTRY_FEES_PERCENT: u128 = 10; +pub const STRAT_CREATOR_FEES_PERCENT: u128 = 10; +pub const TREASURY_FEES_PERCENT: u128 = 80; +pub const POOL_ID_PLACEHOLDER: u64 = 9999; + type FarmId = String; type SeedId = String; -use near_sdk::serde::{Deserialize, Serialize}; + +use fluxus_safe::AccountFee; +use fluxus_safe::FarmInfoBoost; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[serde(crate = "near_sdk::serde")] @@ -34,71 +46,124 @@ pub struct FarmInfo { pub beneficiary_reward: U128, } -const CONTRACT_ID_REF_EXC: &str = "exchange.ref-dev.testnet"; -const CONTRACT_ID_FARM: &str = "farm.leopollum.testnet"; -const FT_CONTRACT_FILEPATH: &str = "./res/fungible_token.wasm"; - -pub async fn deploy_vault_contract(worker: &Worker) -> anyhow::Result { - let wasm = fs::read("res/auto_compounder.wasm").await?; - let contract = worker.dev_deploy(&wasm).await?; +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "near_sdk::serde")] +pub struct PoolInfo { + /// Pool kind. + pub pool_kind: String, + /// List of tokens in the pool. + pub token_account_ids: Vec, + /// How much NEAR this contract has. + pub amounts: Vec, + /// Fee charged for swap. + pub total_fee: u32, + /// Total number of shares. + pub shares_total_supply: U128, + pub amp: u64, +} - contract - .call(&worker, "new_without_pools") +pub async fn add_strategy( + safe_contract: &Contract, + token_reward: &Contract, + pool_id_token1_reward: u64, + pool_id_token2_reward: u64, + pool_id: u64, + farm_id: u64, + worker: &Worker, +) -> anyhow::Result<()> { + let res = safe_contract + .call(worker, "add_farm_to_strategy") .args_json(serde_json::json!({ - "owner_id": contract.id().clone(), + "pool_id": pool_id, + "pool_id_token1_reward": pool_id_token1_reward, + "pool_id_token2_reward": pool_id_token2_reward, + "reward_token": token_reward.id().to_string(), + "farm_id": farm_id.to_string(), }))? .transact() .await?; + assert!(res.is_success()); - Ok(contract) + Ok(()) } - -pub async fn deploy_full_vault_contract( +pub async fn create_strategy( + strat_creator: &Account, + safe_contract: &Contract, token1: &Contract, token2: &Contract, - reward_token: &Contract, - pool_id_token1_reward: u64, - pool_id_token2_reward: u64, pool_id: u64, - farm_id: u64, + worker: &Worker, +) -> anyhow::Result<()> { + let strat: AccountFee = AccountFee { + account_id: strat_creator.id().parse().unwrap(), + fee_percentage: STRAT_CREATOR_FEES_PERCENT, + current_amount: 0, + }; + + let res = safe_contract + .call(worker, "create_strategy") + .args_json(serde_json::json!({ + "_strategy": "".to_string(), + "strategy_fee": TOTAL_PROTOCOL_FEE, + "strat_creator": strat, + "sentry_fee": SENTRY_FEES_PERCENT, + "token1_address": token1.id().to_string(), + "token2_address": token2.id().to_string(), + "pool_id": pool_id, + "seed_min_deposit": MIN_SEED_DEPOSIT.to_string() + }))? + .transact() + .await?; + assert!(res.is_success()); + // println!("create strategy -> {:#?}", res); + + Ok(()) +} + +pub async fn deploy_safe_contract( + owner: &Account, + treasure: &Contract, worker: &Worker, ) -> anyhow::Result { - let wasm = fs::read("res/auto_compounder.wasm").await?; + let wasm = fs::read("res/fluxus_safe.wasm").await?; let contract = worker.dev_deploy(&wasm).await?; - contract - .call(&worker, "new") + let res = contract + .call(worker, "new") .args_json(serde_json::json!({ - "owner_id": contract.id().clone(), - "protocol_shares": 0u128, - "token1_address": token1.id().to_string(), - "token2_address": token2.id().to_string(), - "pool_id_token1_reward": pool_id_token1_reward, - "pool_id_token2_reward": pool_id_token2_reward, - "reward_token": reward_token.id().to_string(), + "owner_id": owner.id(), "exchange_contract_id": CONTRACT_ID_REF_EXC, "farm_contract_id": CONTRACT_ID_FARM, - "farm_id": farm_id, - "pool_id": pool_id, - "seed_min_deposit": U128(MIN_SEED_DEPOSIT) + "treasure_contract_id": treasure.id() }))? .transact() .await?; - /* Register tokens into vault */ - contract - .call(&worker, "extend_whitelisted_tokens") + // println!("deploy safe -> {:#?}", res); + + Ok(contract) +} + +pub async fn deploy_treasure( + owner: &Account, + token_out: &Contract, + worker: &Worker, +) -> anyhow::Result { + let wasm = fs::read("../fluxus-treasurer/res/fluxus_treasurer.wasm").await?; + let contract = worker.dev_deploy(&wasm).await?; + + let res = contract + .call(worker, "new") .args_json(serde_json::json!({ - "tokens": - vec![ - token1.id().clone(), - token2.id().clone(), - reward_token.id().clone(), - ] + "owner_id": owner.id(), + "token_out": token_out.id(), + "exchange_contract_id": CONTRACT_ID_REF_EXC, }))? .transact() .await?; + // println!("deploy treasury -> {:#?}", res); + Ok(contract) } @@ -120,7 +185,7 @@ pub async fn deploy_exchange( // our own set of metadata. This is because the contract's data is too big for the rpc // service to pull down (i.e. greater than 50mb). ref_finance - .call(&worker, "new") + .call(worker, "new") .args_json(serde_json::json!({ "owner_id": ref_finance.id().clone(), "exchange_fee": 4, @@ -138,7 +203,7 @@ pub async fn deploy_exchange( .await?; owner - .call(&worker, ref_finance_id, "storage_deposit") + .call(worker, ref_finance_id, "storage_deposit") .args_json(serde_json::json!({}))? .deposit(parse_near!("20 N")) .transact() @@ -146,7 +211,7 @@ pub async fn deploy_exchange( ref_finance .as_account() - .call(&worker, ref_finance_id, "storage_deposit") + .call(worker, ref_finance_id, "storage_deposit") .args_json(serde_json::json!({}))? .deposit(parse_near!("20 N")) .transact() @@ -155,57 +220,49 @@ pub async fn deploy_exchange( Ok(ref_finance) } -pub async fn deploy_farm( +pub async fn create_farm( owner: &Account, + farm: &Contract, seed_id: &String, token_reward: &Contract, + create_seed: bool, worker: &Worker, -) -> anyhow::Result { - let testnet = workspaces::testnet().await?; - - let farm_acc: AccountId = CONTRACT_ID_FARM.parse().unwrap(); - - let farm = worker - .import_contract(&farm_acc, &testnet) - .transact() - .await?; - - owner - .call(&worker, farm.id(), "new") - .args_json(serde_json::json!({ - "owner_id": owner.id(), - }))? - .transact() - .await?; - - // TODO: remove if not necessary - let _res = farm - .call(&worker, "get_metadata") - .args_json(serde_json::json!({}))? - .deposit(parse_near!("0.1 N")) - .transact() - .await?; - - let reward_per_session: String = parse_near!("0.1 N").to_string(); - owner - .call(&worker, farm.id(), "create_simple_farm") +) -> anyhow::Result<(String, u64)> { + let reward_per_session: String = parse_near!("1000 N").to_string(); + if create_seed { + let res = owner + .call(worker, farm.id(), "create_seed") + .args_json(serde_json::json!({ + "seed_id": seed_id, + "seed_decimal": 24, + "min_locking_duration_sec": 0 + }))? + .deposit(parse_near!("1 yN")) + .gas(parse_gas!("200 Tgas") as u64) + .transact() + .await?; + } + let res = owner + .call(worker, farm.id(), "create_farm") .args_json(serde_json::json!({ + "seed_id": seed_id, "terms": { - "seed_id": seed_id, "reward_token": token_reward.id(), "start_at": 0, - "reward_per_session": reward_per_session, - "session_interval": 60 + "daily_reward": "48000000000000000000" }, "min_deposit": Some(U128(MIN_SEED_DEPOSIT)) }))? - .deposit(parse_near!("0.1 N")) + .deposit(parse_near!("1 yN")) .gas(parse_gas!("200 Tgas") as u64) .transact() .await?; + println!("create_farm: {:#?}", res); + + let farm_id: String = res.json()?; let res = token_reward - .call(&worker, "storage_deposit") + .call(worker, "storage_deposit") .args_json(serde_json::json!({ "account_id": farm.id(), }))? @@ -213,22 +270,51 @@ pub async fn deploy_farm( .gas(parse_gas!("200 Tgas") as u64) .transact() .await?; - // println!("register farm into reward token -> {:#?}", res); + println!("register farm into reward token -> {:#?}", res); + + let amount: String = parse_near!("100000000 N").to_string(); + + let msg = format!("{{\"Reward\":{{\"farm_id\":\"{}\"}} }}", farm_id); + println!("msg {}", msg); - let farm_id = format!("{}#0", seed_id); - let amount: String = parse_near!("20 N").to_string(); let res = owner - .call(&worker, token_reward.id(), "ft_transfer_call") + .call(worker, token_reward.id(), "ft_transfer_call") .args_json(serde_json::json!({ "receiver_id": farm.id(), "amount": amount, - "msg": farm_id + "msg": msg }))? .deposit(parse_near!("1 yN")) .gas(parse_gas!("200 Tgas") as u64) .transact() .await?; - // println!("ft_transfer_call -> {:#?}", res); + println!("ft_transfer_call -> {:#?}", res); + + let id = farm_id.chars().last().unwrap().to_digit(10).unwrap() as u64; + + Ok((farm_id, id)) +} + +pub async fn deploy_farm(owner: &Account, worker: &Worker) -> anyhow::Result { + let testnet = workspaces::testnet().await?; + + let farm_acc: AccountId = CONTRACT_ID_FARM.parse().unwrap(); + + let farm = worker + .import_contract(&farm_acc, &testnet) + .transact() + .await?; + + owner + .call(worker, farm.id(), "new") + .args_json(serde_json::json!({ + "owner_id": owner.id(), + }))? + .transact() + .await?; + + // increase reward per session in order to try to swap in the pool for a value that + // is higher than the pool contains // TODO: require farm state is Running @@ -240,15 +326,12 @@ pub async fn log_farm_info( seed_id: &String, worker: &Worker, ) -> anyhow::Result<()> { - let farm_id = format!("{}#{}", seed_id, 0); - let res = farm - .call(&worker, "get_farm") - .args_json(serde_json::json!({ "farm_id": farm_id }))? - .transact() - .await?; - // TODO: require tx success + let args = serde_json::json!({ "seed_id": seed_id }) + .to_string() + .into_bytes(); - let info: FarmInfo = res.json().unwrap(); + let res = farm.view(worker, "list_seed_farms", args).await?; + let info: Vec = res.json().unwrap(); println!("result {:#?}", info); Ok(()) @@ -306,7 +389,7 @@ pub async fn create_pool_with_liquidity( // println!("register == {:#?}\n", register); let res = owner - .call(&worker, ref_finance.id(), "register_tokens") + .call(worker, ref_finance.id(), "register_tokens") .args_json(serde_json::json!({ "token_ids": token_ids, }))? @@ -316,10 +399,10 @@ pub async fn create_pool_with_liquidity( // println!("register_tokens is {:#?}\n", res); - deposit_tokens(worker, owner, &ref_finance, tokens).await?; + deposit_tokens(worker, owner, ref_finance, tokens).await?; let res = owner - .call(&worker, ref_finance.id(), "add_liquidity") + .call(worker, ref_finance.id(), "add_liquidity") .args_json(serde_json::json!({ "pool_id": pool_id, "amounts": token_amounts, @@ -329,11 +412,11 @@ pub async fn create_pool_with_liquidity( .await?; // println!("added liquidity: {:#?}\n", res); - let res = ref_finance - .call(&worker, "get_pool") - .args_json(serde_json::json!({ "pool_id": pool_id }))? - .transact() - .await?; + // let res = ref_finance + // .call(worker, "get_pool") + // .args_json(serde_json::json!({ "pool_id": pool_id }))? + // .transact() + // .await?; // println!("get pool {:#?}\n", res); @@ -345,42 +428,74 @@ pub async fn create_pools( exchange: &Contract, token_1: &Contract, token_2: &Contract, - token_reward: &Contract, + token_reward_1: &Contract, + token_reward_2: &Contract, + reward_liquidity: u128, + base_liquidity: u128, worker: &Worker, -) -> anyhow::Result<((u64, u64, u64))> { +) -> anyhow::Result<(u64, u64, u64, u64, u64)> { let pool_token1_token2 = create_pool_with_liquidity( - &owner, - &exchange, + owner, + exchange, maplit::hashmap! { - token_1.id() => parse_near!("10 N"), - token_2.id() => parse_near!("10 N"), + token_1.id() => base_liquidity, + token_2.id() => base_liquidity, }, - &worker, + worker, ) .await?; - let pool_token1_reward = create_pool_with_liquidity( - &owner, - &exchange, + let pool_token1_reward1 = create_pool_with_liquidity( + owner, + exchange, maplit::hashmap! { - token_1.id() => parse_near!("10 N"), - token_reward.id() => parse_near!("10 N"), + token_1.id() => base_liquidity, + token_reward_1.id() => reward_liquidity , }, - &worker, + worker, ) .await?; - let pool_token2_reward = create_pool_with_liquidity( - &owner, - &exchange, + let pool_token2_reward1 = create_pool_with_liquidity( + owner, + exchange, maplit::hashmap! { - token_2.id() => parse_near!("10 N"), - token_reward.id() => parse_near!("10 N"), + token_2.id() => base_liquidity, + token_reward_1.id() => reward_liquidity , }, - &worker, + worker, ) .await?; - Ok((pool_token1_token2, pool_token1_reward, pool_token2_reward)) + + let pool_token1_reward2 = create_pool_with_liquidity( + owner, + exchange, + maplit::hashmap! { + token_1.id() => base_liquidity, + token_reward_2.id() => reward_liquidity , + }, + worker, + ) + .await?; + + let pool_token2_reward2 = create_pool_with_liquidity( + owner, + exchange, + maplit::hashmap! { + token_2.id() => base_liquidity, + token_reward_2.id() => reward_liquidity , + }, + worker, + ) + .await?; + + Ok(( + pool_token1_token2, + pool_token1_reward1, + pool_token2_reward1, + pool_token1_reward2, + pool_token2_reward2, + )) } /// Create our own custom Fungible Token contract and setup the initial state. @@ -394,7 +509,7 @@ pub async fn create_custom_ft( // Initialize our FT contract with owner metadata and total supply available // to be traded and transferred into other contracts such as Ref-Finance - ft.call(&worker, "new_default_meta") + ft.call(worker, "new_default_meta") .args_json(serde_json::json!({ "owner_id": owner.id(), "total_supply": parse_near!("1,000,000,000 N").to_string(), @@ -416,7 +531,7 @@ pub async fn deposit_tokens( ) -> anyhow::Result<()> { for (contract_id, amount) in tokens { let res = owner - .call(&worker, contract_id, "ft_transfer_call") + .call(worker, contract_id, "ft_transfer_call") .args_json(serde_json::json!({ "receiver_id": ref_finance.id(), "amount": amount.to_string(), @@ -437,88 +552,117 @@ pub async fn register_into_contracts( contracts_id: Vec<&AccountId>, ) -> anyhow::Result<()> { for contract_id in contracts_id { - account - .call(&worker, &contract_id, "storage_deposit") + let res = account + .call(worker, &contract_id, "storage_deposit") .args_json(serde_json::json!({ "registration_only": false, }))? .deposit(parse_near!("1 N")) .transact() .await?; + + // println!("{:#?}", res); } Ok(()) } +#[allow(dead_code)] pub async fn get_pool_info( worker: &Worker, ref_finance: &Contract, pool_id: u64, ) -> anyhow::Result<()> { - let res = ref_finance - .call(&worker, "get_pool") - .args_json(serde_json::json!({ "pool_id": pool_id }))? - .transact() - .await?; + let args = serde_json::json!({ "pool_id": pool_id }) + .to_string() + .into_bytes(); - println!("get pool {:#?}\n", res); + let res = ref_finance.view(worker, "get_pool", args).await?; + let pool_info: PoolInfo = res.json()?; + println!("get pool {:#?}\n", pool_info); Ok(()) } -pub async fn log_farm_seeds( - auto_compounder: &Contract, +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedInfo { + seed_id: String, + seed_decimal: u64, + next_index: u64, + total_seed_amount: U128, + total_seed_power: U128, + min_deposit: U128, + slash_rate: u64, + min_locking_duration_sec: u64, +} + +#[allow(dead_code)] +pub async fn get_farms_min_deposit( farm: &Contract, worker: &Worker, -) -> anyhow::Result<()> { - let res = farm - .call(&worker, "list_user_seeds") - .args_json(serde_json::json!({ "account_id": auto_compounder.id().to_string() }))? - .transact() - .await?; +) -> anyhow::Result> { + let args = serde_json::json!({ "from_index": 0u64, "limit": 300u64 }) + .to_string() + .into_bytes(); - println!("list_user_seeds {:#?}", res); - Ok(()) + let seeds = farm.view(worker, "list_seeds_info", args).await?; + + let farms_info: Vec = seeds.json()?; + + let mut map: HashMap = HashMap::new(); + + for info in farms_info { + map.insert(info.seed_id, info.min_deposit); + } + + Ok(map) } pub async fn transfer_tokens( from: &Account, - to: &Account, + to: Vec<&Account>, tokens: HashMap<&AccountId, u128>, worker: &Worker, ) -> anyhow::Result<()> { for (token, amount) in tokens.iter() { - let res = to - .call(&worker, token, "storage_deposit") - .args_json(serde_json::json!({ - "registration_only": false, - }))? - .gas(TOTAL_GAS) - .deposit(parse_near!("1 N")) - .transact() - .await?; - // println!("storage_deposit {:#?}\n", res); - - let res = from - .call(&worker, token, "ft_transfer") - .args_json(serde_json::json!({ - "receiver_id": to.id(), - "amount": amount.to_string(), - "msg": Some(""), - }))? - .gas(TOTAL_GAS) - .deposit(parse_near!("1 yN")) - .transact() - .await?; - // println!("ft_transfer {:#?}\n", res); + for receiver in to.iter() { + let res = receiver + .call(worker, token, "storage_deposit") + .args_json(serde_json::json!({ + "registration_only": false, + }))? + .gas(TOTAL_GAS) + .deposit(parse_near!("1 N")) + .transact() + .await?; + // println!("storage_deposit {:#?}\n", res); + + let res = from + .call(worker, token, "ft_transfer") + .args_json(serde_json::json!({ + "receiver_id": receiver.id(), + "amount": amount.to_string(), + "msg": Some(""), + }))? + .gas(TOTAL_GAS) + .deposit(parse_near!("1 yN")) + .transact() + .await?; + // println!("ft_transfer {:#?}\n", res); + } } Ok(()) } -pub fn str_to_u128(amount: &String) -> u128 { +pub fn str_to_u128(amount: &str) -> u128 { amount.parse::().unwrap() } +pub fn str_to_i128(amount: &str) -> i128 { + amount.parse::().unwrap() +} pub async fn get_pool_shares( account: &Account, @@ -526,17 +670,150 @@ pub async fn get_pool_shares( pool_id: u64, worker: &Worker, ) -> anyhow::Result { - let res = account - .call(&worker, exchange.id(), "get_pool_shares") + let args = serde_json::json!({ "pool_id": pool_id, "account_id": account.id().to_string() }) + .to_string() + .into_bytes(); + + let res = exchange.view(worker, "get_pool_shares", args).await?; + let shares: String = res.json()?; + + Ok(shares) +} + +pub async fn get_balance_of( + account: &Account, + contract: &Contract, + is_ft: bool, + worker: &Worker, + mft_id: Option, +) -> anyhow::Result { + let (function_str, args) = if is_ft { + ( + "ft_balance_of", + serde_json::json!({"account_id": account.id()}) + .to_string() + .into_bytes(), + ) + } else { + ( + "mft_balance_of", + serde_json::json!({"token_id": mft_id.unwrap(), "account_id": account.id()}) + .to_string() + .into_bytes(), + ) + }; + + let res: U128 = contract.view(worker, function_str, args).await?.json()?; + Ok(res) +} + +#[allow(dead_code)] +pub async fn get_unclaimed_rewards( + contract: &Contract, + farm_id_str: &String, + worker: &Worker, +) -> anyhow::Result { + let unclaimed_amount: U128 = contract + .call(worker, "get_unclaimed_rewards") + .args_json(serde_json::json!({ "farm_id_str": farm_id_str }))? + .gas(TOTAL_GAS) + .transact() + .await? + .json()?; + Ok(unclaimed_amount.0) +} + +#[allow(dead_code)] +pub async fn create_account_and_add_liquidity( + owner: &Account, + contract: &Contract, + exchange: &Contract, + pool_id: u64, + token_1: &Contract, + token_2: &Contract, + token_id: &String, + base_liquidity: u128, + worker: &Worker, +) -> anyhow::Result { + let new_account = worker.dev_create_account().await?; + + register_into_contracts(worker, &new_account, vec![exchange.id()]).await?; + + // Transfer from owner to new account + transfer_tokens( + owner, + vec![&new_account], + maplit::hashmap! { + token_1.id() => parse_near!("10,000 N"), + token_2.id() => parse_near!("10,000 N"), + }, + worker, + ) + .await?; + + let res = owner + .call(worker, exchange.id(), "add_liquidity") .args_json(serde_json::json!({ "pool_id": pool_id, - "account_id": account.id().to_string() + "amounts": maplit::hashmap! { + token_1.id() => base_liquidity, + token_2.id() => base_liquidity, + }, }))? - .gas(TOTAL_GAS) + .deposit(parse_near!("1 N")) .transact() .await?; - // println!("get_pool_shares {:#?}\n", res); + // println!("added liquidity: {:#?}\n", res); + Ok(0) +} - let shares: String = res.json()?; - Ok(shares) +pub async fn get_user_fft( + contract: &Contract, + account: &Account, + fft_id: &String, + worker: &Worker, +) -> anyhow::Result { + let args = serde_json::json!({ "fft_share": fft_id, "user": account.id().to_string(), }) + .to_string() + .into_bytes(); + let res = contract + .view(worker, "users_fft_share_amount", args) + .await?; + + let account_shares: u128 = res.json()?; + Ok(account_shares) +} + +pub async fn get_fft_token_by_seed( + safe_contract: &Contract, + seed_id: &String, + worker: &Worker, +) -> anyhow::Result { + let args = serde_json::json!({ "seed_id": seed_id }) + .to_string() + .into_bytes(); + + let fft_token: String = safe_contract + .view(worker, "fft_token_seed_id", args) + .await? + .json()?; + + Ok(fft_token) +} + +pub async fn get_seed_total_amount( + safe_contract: &Contract, + token_id: &String, + worker: &Worker, +) -> anyhow::Result { + let args = serde_json::json!({ "token_id": token_id }) + .to_string() + .into_bytes(); + + let seed_before_withdraw = safe_contract + .view(worker, "seed_total_amount", args) + .await? + .json()?; + + Ok(seed_before_withdraw) } diff --git a/fluxus-treasurer/Cargo.toml b/fluxus-treasurer/Cargo.toml index 1234708..b1a60fa 100644 --- a/fluxus-treasurer/Cargo.toml +++ b/fluxus-treasurer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fluxus_treasurer" -version = "0.1.0" +version = "0.1.1" authors = ["Pollum"] edition = "2021" diff --git a/fluxus-treasurer/res/fluxus_treasurer.wasm b/fluxus-treasurer/res/fluxus_treasurer.wasm index d4a89a6..580b1cc 100644 Binary files a/fluxus-treasurer/res/fluxus_treasurer.wasm and b/fluxus-treasurer/res/fluxus_treasurer.wasm differ diff --git a/fluxus-treasurer/scripts/initialize.sh b/fluxus-treasurer/scripts/initialize.sh index cd074c3..86e7fea 100644 --- a/fluxus-treasurer/scripts/initialize.sh +++ b/fluxus-treasurer/scripts/initialize.sh @@ -6,7 +6,7 @@ echo $username #### Initialize contract -near call $CONTRACT_NAME new '{"owner_id":"'$username'", "token_out": "'$token_out'", "exchange_contract_id": "'$exchange_contract_id'"}' --accountId $username +near call $CONTRACT_NAME new '{"owner_id":'$username', "token_out": "'$token_out'", "exchange_contract_id": "ref-finance-101.testnet"}' --accountId leopollum.testnet #### Register contract @@ -21,7 +21,7 @@ near call $token_out storage_deposit '{"account_id": "'$CONTRACT_NAME'", "regist near call $CONTRACT_NAME register_token '{ "token" : "'$token_in'", "pool_id": '$pool_token_in' }' --accountId $CONTRACT_NAME --gas $total_gas # Add stakeholder account -near call $CONTRACT_NAME add_stakeholder '{ "account_id": "'$username'", "fee": '$fee' }' --accountId $CONTRACT_NAME +near call $CONTRACT_NAME add_stakeholder '{ "account_id": '$username', "fee": '$fee' }' --accountId $CONTRACT_NAME # Get contracts stakeholders near call $CONTRACT_NAME get_stakeholders '{}' --accountId $CONTRACT_NAME diff --git a/fluxus-treasurer/src/lib.rs b/fluxus-treasurer/src/lib.rs index 88089f6..62d0fcd 100644 --- a/fluxus-treasurer/src/lib.rs +++ b/fluxus-treasurer/src/lib.rs @@ -1,8 +1,10 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::json_types::U128; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::PromiseError; -use near_sdk::{env, ext_contract, near_bindgen, require, AccountId, Gas, PanicOnDefault, Promise}; +use near_sdk::{ + env, ext_contract, near_bindgen, require, AccountId, Gas, PanicOnDefault, Promise, PromiseIndex, +}; +use near_sdk::{PromiseError, PromiseOrValue}; use std::collections::HashMap; use std::convert::Into; @@ -33,7 +35,7 @@ impl fmt::Display for RunningState { } #[near_bindgen] #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] -pub struct Contract { +pub struct ContractData { // Account address that have authority to update the contract state owner_id: AccountId, @@ -57,7 +59,6 @@ pub struct Contract { // Functions that we need to call like a callback. #[ext_contract(ext_self)] pub trait ExtContract { - fn callback_swap(&self, #[callback_result] balance: Result) -> String; fn callback_register_token(&self, token: AccountId, pool_id: u64) -> String; fn get_token_return_and_swap( &self, @@ -72,15 +73,15 @@ pub trait ExtContract { amount_in: U128, pool_id: u64, ) -> Promise; - fn callback_balance_of( + fn callback_post_swap( &self, #[callback_result] withdraw_result: Result<(), PromiseError>, ) -> Promise; fn internal_distribute( &mut self, - #[callback_result] withdraw_result: Result<(), PromiseError>, + #[callback_result] withdraw_result: Result, amount: U128, - ) -> String; + ) -> PromiseOrValue; fn callback_withdraw( &mut self, #[callback_result] transfer_result: Result<(), PromiseError>, @@ -90,33 +91,12 @@ pub trait ExtContract { #[near_bindgen] impl Contract { - /// Function that initialize the contract. - /// - /// Arguments: - /// - /// - `owner_id` - The account id that owns the contract - /// - `token_out` - Token address, used to distribute fees between stakeholders - /// - `exchange_contract_id` - The exchange that will be used to swap tokens - /// - #[init] - pub fn new(owner_id: AccountId, token_out: AccountId, exchange_contract_id: AccountId) -> Self { - Self { - owner_id: owner_id, - stakeholders_fees: HashMap::new(), - stakeholders_amount_available: HashMap::new(), - token_out, - token_to_pool: HashMap::new(), - state: RunningState::Running, - exchange_contract_id, - } - } - /// Function responsible for swapping rewards tokens for the token distributed - pub fn execute_swaps(&self, token: AccountId) -> Promise { + pub fn execute_swaps_and_distribute(&self, token: AccountId) -> Promise { // self.assert_contract_running(); // self.is_owner(); - let token_pool = self.token_to_pool.get(&token); + let token_pool = self.data().token_to_pool.get(&token); let mut pool: u64 = 0; match token_pool { @@ -129,7 +109,7 @@ impl Contract { ext_exchange::get_deposit( env::current_account_id(), token.clone(), - self.exchange_contract_id.clone(), + self.exchange_acc(), 1, Gas(9_000_000_000_000), ) @@ -140,10 +120,10 @@ impl Contract { 0, Gas(70_000_000_000_000), )) - .then(ext_self::callback_swap( + .then(ext_self::callback_post_swap( env::current_account_id(), 0, - Gas(9_000_000_000_000), + Gas(100_000_000_000_000), )) } @@ -165,8 +145,8 @@ impl Contract { pool_id, token.clone(), amount_in, - self.token_out.clone(), - self.exchange_contract_id.clone(), + self.data().token_out.clone(), + self.exchange_acc(), 0, Gas(10_000_000_000_000), ) @@ -180,7 +160,7 @@ impl Contract { )) } - /// Swaps the token received by execute_swaps for token_out + /// Swaps the token received by execute_swaps_and_distribute for token_out #[private] pub fn swap( &self, @@ -208,46 +188,19 @@ impl Contract { vec![SwapAction { pool_id: pool_id, token_in: token_in, - token_out: self.token_out.clone(), + token_out: self.data().token_out.clone(), amount_in: Some(amount_in), min_amount_out: U128(min_amount_out), }], None, - self.exchange_contract_id.clone(), + self.exchange_acc(), 1, Gas(20_000_000_000_000), ) } - /// Callback to ensure that the swap call was successful - #[private] - pub fn callback_swap(&self, #[callback_result] balance: Result) -> String { - assert!(balance.is_ok(), "TREASURER::SWAP_FAILED"); - - let amount: u128 = balance.unwrap().into(); - format!("Treasurer received {} wNEAR", amount) - } - - /// Get amount from exchange, withdraw it and distribute amount between stakeholders - pub fn distribute(&self) -> Promise { - self.is_owner(); - - ext_exchange::get_deposit( - env::current_account_id(), - self.token_out.clone(), - self.exchange_contract_id.clone(), - 0, - Gas(10_000_000_000_000), - ) - .then(ext_self::callback_balance_of( - env::current_account_id(), - 0, - Gas(100_000_000_000_000), - )) - } - #[private] - pub fn callback_balance_of( + pub fn callback_post_swap( &self, #[callback_result] deposit_result: Result, ) -> Promise { @@ -256,10 +209,10 @@ impl Contract { let amount = deposit_result.unwrap(); ext_exchange::withdraw( - self.token_out.to_string(), + self.data().token_out.to_string(), amount.clone(), Some(false), - self.exchange_contract_id.clone(), + self.exchange_acc(), 1, Gas(60_000_000_000_000), ) @@ -267,16 +220,16 @@ impl Contract { amount, env::current_account_id(), 0, - Gas(30_000_000_000_000), + Gas(20_000_000_000_000), )) } #[private] pub fn internal_distribute( &mut self, - #[callback_result] withdraw_result: Result<(), PromiseError>, + #[callback_result] withdraw_result: Result, amount: U128, - ) -> String { + ) -> PromiseOrValue { assert!(withdraw_result.is_ok(), "TREASURER::ERR_CANNOT_GET_BALANCE"); let total_amount: u128 = amount.into(); @@ -287,7 +240,7 @@ impl Contract { // let mut stakeholders_amounts: Vec = Vec::new(); let mut stakeholders_amounts: HashMap = HashMap::new(); - for (account, perc) in self.stakeholders_fees.clone() { + for (account, perc) in self.data().stakeholders_fees.clone() { let percent = Percentage::from(perc); let amount_received: u128 = percent.apply_to(total_amount); @@ -301,21 +254,27 @@ impl Contract { stakeholders_amounts.insert(account, amount_received); } + // TODO: if this goes wrong, the value was already withdraw and there is no way to distribute again assert!( total_distributed <= total_amount, "TREASURER::ERR_TRIED_TO_DISTRIBUTE_HIGHER_AMOUNT" ); for (acc, amount) in stakeholders_amounts.clone() { - let prev_amount: &u128 = self.stakeholders_amount_available.get(&acc).unwrap(); + let prev_amount: &u128 = self + .data_mut() + .stakeholders_amount_available + .get(&acc) + .unwrap(); let current_amount: u128 = prev_amount + amount; - self.stakeholders_amount_available + self.data_mut() + .stakeholders_amount_available .insert(acc, current_amount); } - format!("Stakeholders can already withdraw from Treasurer") + PromiseOrValue::Value("Stakeholders can already withdraw from Treasurer".to_string()) } /// Transfer caller's current available amount from contract to caller @@ -323,11 +282,15 @@ impl Contract { let (caller_id, contract_id) = self.get_predecessor_and_current_account(); assert!( - self.stakeholders_fees.contains_key(&caller_id), + self.data().stakeholders_fees.contains_key(&caller_id), "TREASURER::ERR_ACCOUNT_DOES_NOT_EXIST" ); - let amount: &u128 = self.stakeholders_amount_available.get(&caller_id).unwrap(); + let amount: &u128 = self + .data() + .stakeholders_amount_available + .get(&caller_id) + .unwrap(); assert_ne!(*amount, 0u128, "TREASURER::ERR_WITHDRAW_ZERO_AMOUNT"); @@ -335,7 +298,7 @@ impl Contract { caller_id.clone(), U128(*amount), Some(String::from("")), - self.token_out.clone(), + self.data().token_out.clone(), 1, Gas(100_000_000_000_000), ) @@ -359,7 +322,8 @@ impl Contract { "TREASURER::ERR_WITHDRAW_FROM_CONTRACT_FAILED" ); - self.stakeholders_amount_available + self.data_mut() + .stakeholders_amount_available .insert(account_id.clone(), 0u128); format!("The withdraw from {} was successfully", account_id) @@ -370,7 +334,7 @@ impl Contract { pub fn is_owner(&self) { let (caller_acc_id, contract_id) = self.get_predecessor_and_current_account(); require!( - caller_acc_id == contract_id || caller_acc_id == self.owner_id, + caller_acc_id == contract_id || caller_acc_id == self.data().owner_id, "TREASURER::ERR_NOT_ALLOWED" ); } @@ -383,18 +347,79 @@ impl Contract { #[private] pub fn assert_contract_running(&self) { - match self.state { + match self.data().state { RunningState::Running => (), _ => env::panic_str("TREASURER::CONTRACT_PAUSED"), }; } pub fn update_contract_state(&mut self, state: RunningState) -> String { - self.state = state; - format!("{} is {}", env::current_account_id(), self.state) + self.data_mut().state = state; + format!("{} is {}", env::current_account_id(), self.data().state) } pub fn get_contract_state(&self) -> String { - format!("{} is {}", env::current_account_id(), self.state) + format!("{} is {}", env::current_account_id(), self.data().state) + } +} +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct Contract { + data: VersionedContractData, +} + +/// Versioned contract data. Allows to easily upgrade contracts. +#[derive(BorshSerialize, BorshDeserialize)] +pub enum VersionedContractData { + V0001(ContractData), +} + +impl VersionedContractData {} + +#[near_bindgen] +impl Contract { + /// Function that initialize the contract. + /// + /// Arguments: + /// + /// - `owner_id` - The account id that owns the contract + /// - `token_out` - Token address, used to distribute fees between stakeholders + /// - `exchange_contract_id` - The exchange that will be used to swap tokens + #[init] + pub fn new(owner_id: AccountId, token_out: AccountId, exchange_contract_id: AccountId) -> Self { + assert!(!env::state_exists(), "Already initialized"); + let allowed_accounts: Vec = vec![env::current_account_id()]; + + Self { + data: VersionedContractData::V0001(ContractData { + owner_id, + stakeholders_fees: HashMap::new(), + stakeholders_amount_available: HashMap::new(), + token_out, + token_to_pool: HashMap::new(), + state: RunningState::Running, + exchange_contract_id, + }), + } + } +} + +impl Contract { + fn data(&self) -> &ContractData { + match &self.data { + VersionedContractData::V0001(data) => data, + _ => unimplemented!(), + } + } + + fn data_mut(&mut self) -> &mut ContractData { + match &mut self.data { + VersionedContractData::V0001(data) => data, + _ => unimplemented!(), + } + } + + fn exchange_acc(&self) -> AccountId { + self.data().exchange_contract_id.clone() } } diff --git a/fluxus-treasurer/src/managed_tokens.rs b/fluxus-treasurer/src/managed_tokens.rs index 66f6a32..62016f2 100644 --- a/fluxus-treasurer/src/managed_tokens.rs +++ b/fluxus-treasurer/src/managed_tokens.rs @@ -8,14 +8,14 @@ impl Contract { pub fn register_token(&mut self, token: AccountId, pool_id: u64) -> Promise { self.is_owner(); assert_eq!( - self.token_to_pool.contains_key(&token), + self.data().token_to_pool.contains_key(&token), false, "TREASURER::ERR_TOKEN_ALREADY_EXIST" ); ext_exchange::register_tokens( vec![token.clone()], - self.exchange_contract_id.clone(), + self.exchange_acc(), 1, Gas(20_000_000_000_000), ) @@ -47,7 +47,9 @@ impl Contract { assert!(register_result.is_ok(), "TREASURER::COULD_NOT_REGISTER"); assert!(deposit_result.is_ok(), "TREASURER::COULD_NOT_DEPOSIT"); - self.token_to_pool.insert(token.clone(), pool_id.clone()); + self.data_mut() + .token_to_pool + .insert(token.clone(), pool_id.clone()); format!( "The token {} with pool {} was added successfully", @@ -59,11 +61,11 @@ impl Contract { pub fn update_token_pool(&mut self, token: AccountId, pool_id: u64) -> String { self.is_owner(); assert!( - self.token_to_pool.contains_key(&token), + self.data().token_to_pool.contains_key(&token), "TREASURER::ERR_TOKEN_DOES_NOT_EXIST" ); - self.token_to_pool.insert(token.clone(), pool_id); + self.data_mut().token_to_pool.insert(token.clone(), pool_id); format!( "The token {} with pool {} was updated successfully", @@ -72,7 +74,7 @@ impl Contract { } pub fn get_registered_tokens(&self) -> HashMap { - self.token_to_pool.clone() + self.data().token_to_pool.clone() } } @@ -116,7 +118,10 @@ mod tests { let token = to_account_id("usn.near"); let pool_id = 100u64; - contract.token_to_pool.insert(token.clone(), pool_id); + contract + .data_mut() + .token_to_pool + .insert(token.clone(), pool_id); contract.update_token_pool(token, pool_id + 1); let token2 = to_account_id("dai.near"); @@ -135,7 +140,10 @@ mod tests { let token = to_account_id("usn.near"); let pool_id = 100u64; - contract.token_to_pool.insert(token.clone(), pool_id); + contract + .data_mut() + .token_to_pool + .insert(token.clone(), pool_id); let registered_tokens: HashMap = contract.get_registered_tokens(); assert_eq!( diff --git a/fluxus-treasurer/src/stakeholders.rs b/fluxus-treasurer/src/stakeholders.rs index e047ce2..7668cfa 100644 --- a/fluxus-treasurer/src/stakeholders.rs +++ b/fluxus-treasurer/src/stakeholders.rs @@ -9,7 +9,7 @@ impl Contract { let mut total_fees: u128 = 0u128; - for (acc_id, account_fee) in self.stakeholders_fees.iter() { + for (acc_id, account_fee) in self.data().stakeholders_fees.iter() { assert!( *acc_id != account_id, "TREASURER::ERR_ADDRESS_ALREADY_EXIST" @@ -24,8 +24,11 @@ impl Contract { "TREASURER::ERR_FEE_EXCEEDS_MAXIMUM_VALUE" ); - self.stakeholders_fees.insert(account_id.clone(), fee); - self.stakeholders_amount_available + self.data_mut() + .stakeholders_fees + .insert(account_id.clone(), fee); + self.data_mut() + .stakeholders_amount_available .insert(account_id.clone(), 0u128); format!( @@ -37,7 +40,7 @@ impl Contract { /// Removes account from stakeholders_fee pub fn remove_stakeholder(&mut self, account_id: AccountId) { self.is_owner(); - self.stakeholders_fees.remove(&account_id); + self.data_mut().stakeholders_fees.remove(&account_id); } pub fn update_stakeholder_percentage( @@ -47,12 +50,12 @@ impl Contract { ) -> String { self.is_owner(); assert!( - self.stakeholders_fees.contains_key(&account_id), + self.data().stakeholders_fees.contains_key(&account_id), "TREASURER::ERR_ACCOUNT_DOES_NOT_EXIST" ); let mut total_fees = new_percentage; - for (account, percentage) in self.stakeholders_fees.iter() { + for (account, percentage) in self.data().stakeholders_fees.iter() { if account != &account_id { total_fees += percentage; } @@ -63,7 +66,8 @@ impl Contract { "TREASURER::ERR_FEE_EXCEEDS_MAXIMUM_VALUE" ); - self.stakeholders_fees + self.data_mut() + .stakeholders_fees .insert(account_id.clone(), new_percentage); format! { "The percentage for {} is now {}", account_id, new_percentage} @@ -72,7 +76,7 @@ impl Contract { /// Returns stakeholders and associated fees pub fn get_stakeholders(&self) -> HashMap { self.is_owner(); - self.stakeholders_fees.clone() + self.data().stakeholders_fees.clone() } }