diff --git a/Cargo.lock b/Cargo.lock index adf607c..00a886e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "ref-exchange" -version = "1.9.3" +version = "1.9.4" dependencies = [ "hex", "mock-boost-farming", diff --git a/ref-exchange/Cargo.toml b/ref-exchange/Cargo.toml index c9ac2e9..15e0b9f 100644 --- a/ref-exchange/Cargo.toml +++ b/ref-exchange/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ref-exchange" -version = "1.9.3" +version = "1.9.4" authors = ["Illia Polosukhin "] edition = "2018" publish = false diff --git a/ref-exchange/release_notes.md b/ref-exchange/release_notes.md index 15da59f..f221760 100644 --- a/ref-exchange/release_notes.md +++ b/ref-exchange/release_notes.md @@ -1,5 +1,12 @@ # Release Notes +### Version 1.9.4 +``` +EcvZHyadAraTekV5isk2yucMFQvpSHG7uciCNuacDiau +``` +1. add pool limit +2. add execute_actions_in_va + ### Version 1.9.3 ``` 1PW1wtYsciZKsaRNqNMpY3P1W2wD42PjZVraL142VN4 diff --git a/ref-exchange/src/custom_keys.rs b/ref-exchange/src/custom_keys.rs index 974c1ad..6685a62 100644 --- a/ref-exchange/src/custom_keys.rs +++ b/ref-exchange/src/custom_keys.rs @@ -4,3 +4,6 @@ pub const RATE_STORAGE_KEY: &str = "custom_rate_key"; // Key for degen token info pub const DEGEN_STORAGE_KEY: &str = "custom_degen_key"; pub const DEGEN_ORACLE_CONFIG_STORAGE_KEY: &str = "custom_degen_oracle_config_key"; + +// Key for pool limit +pub const POOL_LIMIT: &str = "pl"; \ No newline at end of file diff --git a/ref-exchange/src/degen_swap/math.rs b/ref-exchange/src/degen_swap/math.rs index d5c2f38..933e52c 100644 --- a/ref-exchange/src/degen_swap/math.rs +++ b/ref-exchange/src/degen_swap/math.rs @@ -134,7 +134,7 @@ impl DegenSwap { } /// * - fn degen_balances(&self, amounts: &Vec) -> Vec { + pub fn degen_balances(&self, amounts: &Vec) -> Vec { amounts.iter().zip(self.degens.iter()).map(|(&amount, °en)| { self.mul_degen(amount, degen) }).collect() diff --git a/ref-exchange/src/degen_swap/mod.rs b/ref-exchange/src/degen_swap/mod.rs index 1b33e25..e63a611 100644 --- a/ref-exchange/src/degen_swap/mod.rs +++ b/ref-exchange/src/degen_swap/mod.rs @@ -201,6 +201,12 @@ impl DegenSwapPool { .as_u128() } + pub fn get_tvl(&self) -> u128 { + self.get_invariant_with_degens(&self.get_degens()) + .degen_balances(&self.c_amounts).iter() + .map(|v| v / 10u128.pow(TARGET_DECIMAL.into())).sum() + } + /// caculate mint share and related fee for adding liquidity /// return (share, fee_part) fn calc_add_liquidity_with_degens( diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index 8ec8340..b451fed 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -28,11 +28,12 @@ use crate::rated_swap::{RatedSwapPool, rate::{RateTrait, global_get_rate, global use crate::utils::{check_token_duplicates, pair_rated_price_to_vec_u8, TokenCache}; pub use crate::custom_keys::*; pub use crate::views::{PoolInfo, ShadowRecordInfo, RatedPoolInfo, StablePoolInfo, ContractMetadata, RatedTokenInfo, DegenTokenInfo, AddLiquidityPrediction, RefStorageState}; -pub use crate::token_receiver::AddLiquidityInfo; +pub use crate::token_receiver::{AddLiquidityInfo, VIRTUAL_ACC}; pub use crate::shadow_actions::*; pub use crate::unit_lpt_cumulative_infos::*; pub use crate::oracle::*; pub use crate::degen_swap::*; +pub use crate::pool_limit_info::*; mod account_deposit; mod action; @@ -54,6 +55,7 @@ mod views; mod custom_keys; mod shadow_actions; mod unit_lpt_cumulative_infos; +mod pool_limit_info; near_sdk::setup_alloc!(); @@ -69,6 +71,7 @@ pub(crate) enum StorageKey { Referral, ShadowRecord {account_id: AccountId}, UnitShareCumulativeInfo, + PoolLimit, } #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Eq, PartialEq, Clone)] @@ -226,6 +229,59 @@ impl Contract { ))) } + #[payable] + pub fn execute_actions_in_va( + &mut self, + use_tokens: HashMap, + actions: Vec, + referral_id: Option, + ) -> HashMap { + self.assert_contract_running(); + assert_ne!(actions.len(), 0, "{}", ERR72_AT_LEAST_ONE_SWAP); + let sender_id = env::predecessor_account_id(); + let mut account = self.internal_unwrap_account(&sender_id); + // Validate that all tokens are whitelisted if no deposit (e.g. trade with access key). + if env::attached_deposit() == 0 { + for action in &actions { + for token in action.tokens() { + assert!( + account.get_balance(&token).is_some() + || self.is_whitelisted_token(&token), + "{}", + // [AUDIT_05] + ERR27_DEPOSIT_NEEDED + ); + } + } + } + + let mut virtual_account: Account = Account::new(&String::from(VIRTUAL_ACC)); + let referral_info :Option<(AccountId, u32)> = referral_id + .as_ref().and_then(|rid| self.referrals.get(rid.as_ref())) + .map(|fee| (referral_id.unwrap().into(), fee)); + for (use_token, use_amount) in use_tokens.iter() { + account.withdraw(use_token, use_amount.0); + virtual_account.deposit(use_token, use_amount.0); + } + let _ = self.internal_execute_actions( + &mut virtual_account, + &referral_info, + &actions, + ActionResult::None, + ); + let mut result = HashMap::new(); + for (token, amount) in virtual_account.tokens.to_vec() { + if amount > 0 { + account.deposit(&token, amount); + result.insert(token, amount.into()); + } + } + + virtual_account.tokens.clear(); + self.internal_save_account(&sender_id, account); + result + } + /// [AUDIT_03_reject(NOPE action is allowed by design)] /// [AUDIT_04] /// Executes generic set of actions. @@ -377,6 +433,11 @@ impl Contract { AdminFees::new(self.admin_fee_bps), false ); + if pool.kind() == "DEGEN_SWAP" { + if let Some(degen_pool_limit) = read_pool_limit_from_storage().get(&pool_id).map(|v| v.get_degen_pool_limit()) { + assert!(pool.get_tvl() <= degen_pool_limit.tvl_limit, "Exceed Max TVL"); + } + } // [AUDITION_AMENDMENT] 2.3.7 Code Optimization (I) let mut deposits = self.internal_unwrap_account(&sender_id); let tokens = pool.tokens(); diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index 8becfe9..a3b5fe9 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -446,6 +446,37 @@ impl Contract { } } + #[payable] + pub fn add_degen_pool_limit(&mut self, pool_id: u64, degen_pool_limit_info: DegenPoolLimitInfo) { + assert_one_yocto(); + assert!(self.is_owner_or_guardians(), "{}", ERR100_NOT_ALLOWED); + assert!(self.get_pool(pool_id).pool_kind == "DEGEN_SWAP"); + let mut pool_limit = read_pool_limit_from_storage(); + assert!(pool_limit.get(&pool_id).is_none(), "degen pool limit already exist"); + pool_limit.insert(&pool_id, &VPoolLimitInfo::DegenPoolLimit(degen_pool_limit_info.into())); + write_pool_limit_to_storage(pool_limit); + } + + #[payable] + pub fn update_degen_pool_limit(&mut self, pool_id: u64, degen_pool_limit_info: DegenPoolLimitInfo) { + assert_one_yocto(); + self.assert_owner(); + assert!(self.get_pool(pool_id).pool_kind == "DEGEN_SWAP"); + let mut pool_limit = read_pool_limit_from_storage(); + assert!(pool_limit.get(&pool_id).is_some(), "degen pool limit not exist"); + pool_limit.insert(&pool_id, &VPoolLimitInfo::DegenPoolLimit(degen_pool_limit_info.into())); + write_pool_limit_to_storage(pool_limit); + } + + #[payable] + pub fn remove_pool_limit(&mut self, pool_id: u64) { + assert_one_yocto(); + self.assert_owner(); + let mut pool_limit = read_pool_limit_from_storage(); + assert!(pool_limit.remove(&pool_id).is_some(), "Invalid pool_id"); + write_pool_limit_to_storage(pool_limit); + } + pub(crate) fn assert_owner(&self) { assert_eq!( env::predecessor_account_id(), diff --git a/ref-exchange/src/pool.rs b/ref-exchange/src/pool.rs index f518309..4171f7a 100644 --- a/ref-exchange/src/pool.rs +++ b/ref-exchange/src/pool.rs @@ -165,6 +165,15 @@ impl Pool { } } + pub fn get_tvl(&self) -> u128 { + match self { + Pool::SimplePool(_) => unimplemented!(), + Pool::StableSwapPool(_) => unimplemented!(), + Pool::RatedSwapPool(_) => unimplemented!(), + Pool::DegenSwapPool(pool) => pool.get_tvl(), + } + } + /// Swaps given number of token_in for token_out and returns received amount. pub fn swap( &mut self, diff --git a/ref-exchange/src/pool_limit_info.rs b/ref-exchange/src/pool_limit_info.rs new file mode 100644 index 0000000..2bc7268 --- /dev/null +++ b/ref-exchange/src/pool_limit_info.rs @@ -0,0 +1,62 @@ +use crate::*; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use crate::utils::u128_dec_format; + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub struct DegenPoolLimitInfo { + #[serde(with = "u128_dec_format")] + pub tvl_limit: u128 +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub enum VDegenPoolLimitInfo { + Current(DegenPoolLimitInfo), +} + +impl From for DegenPoolLimitInfo { + fn from(v: VDegenPoolLimitInfo) -> Self { + match v { + VDegenPoolLimitInfo::Current(c) => c, + } + } +} + +impl From for VDegenPoolLimitInfo { + fn from(c: DegenPoolLimitInfo) -> Self { + VDegenPoolLimitInfo::Current(c) + } +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub enum VPoolLimitInfo { + DegenPoolLimit(VDegenPoolLimitInfo) +} + +impl VPoolLimitInfo { + pub fn get_degen_pool_limit(self) -> DegenPoolLimitInfo { + match self { + VPoolLimitInfo::DegenPoolLimit(l) => l.into(), + } + } +} + +pub fn read_pool_limit_from_storage() -> UnorderedMap { + if let Some(content) = env::storage_read(POOL_LIMIT.as_bytes()) { + UnorderedMap::try_from_slice(&content).expect("deserialize pool limit info failed.") + } else { + UnorderedMap::new(StorageKey::PoolLimit) + } +} + +pub fn write_pool_limit_to_storage(pool_limit: UnorderedMap) { + env::storage_write( + POOL_LIMIT.as_bytes(), + &pool_limit.try_to_vec().unwrap(), + ); +} diff --git a/ref-exchange/src/views.rs b/ref-exchange/src/views.rs index be99ff1..2e9201b 100644 --- a/ref-exchange/src/views.rs +++ b/ref-exchange/src/views.rs @@ -844,4 +844,25 @@ impl Contract { Some((add_liquidity_predictions, token_cache.into())) } + + pub fn get_degen_pool_tvl(&self, pool_id: u64) -> U128 { + self.pools.get(pool_id).expect(ERR85_NO_POOL).get_tvl().into() + } + + pub fn get_pool_limit_by_pool_id(&self, pool_id: u64) -> Option { + read_pool_limit_from_storage().get(&pool_id) + } + + pub fn get_pool_limit_paged(&self, from_index: Option, limit: Option) -> HashMap { + let pool_limit = read_pool_limit_from_storage(); + let keys = pool_limit.keys_as_vector(); + let from_index = from_index.unwrap_or(0); + let limit = limit.unwrap_or(keys.len() as u64); + (from_index..std::cmp::min(keys.len() as u64, from_index + limit)) + .map(|idx| { + let key = keys.get(idx).unwrap(); + (key.clone(), pool_limit.get(&key).unwrap()) + }) + .collect() + } } diff --git a/ref-exchange/tests/test_degen_pool.rs b/ref-exchange/tests/test_degen_pool.rs index f2ca7d1..e17fc30 100644 --- a/ref-exchange/tests/test_degen_pool.rs +++ b/ref-exchange/tests/test_degen_pool.rs @@ -3,7 +3,7 @@ use mock_pyth::PythPrice; use near_contract_standards::fungible_token::metadata::FungibleTokenMetadata; use near_sdk::{json_types::U128, AccountId}; use near_sdk_sim::{call, to_yocto, view}; -use ref_exchange::{DegenOracleConfig, DegenTokenInfo, DegenType, PoolInfo, PriceOracleConfig, PythOracleConfig, SwapAction}; +use ref_exchange::{DegenOracleConfig, DegenPoolLimitInfo, DegenTokenInfo, DegenType, PoolInfo, PriceOracleConfig, PythOracleConfig, SwapAction, VPoolLimitInfo}; use std::{collections::HashMap, convert::TryInto}; use crate::common::utils::*; pub mod common; @@ -330,3 +330,119 @@ fn sim_degen1() { assert_eq!(balances[&btc()].0, 0); assert_eq!(balances[ð()].0, 997499999501274936); } + +#[test] +fn degen_limit() { + let (root, owner, pool, _tokens) = + setup_degen_pool( + vec![eth(), near()], + vec![100000*ONE_ETH, 100000*ONE_NEAR], + vec![18, 24], + 25, + 10000, + ); + let price_oracle_contract = setup_price_oracle(&root); + call!( + root, + price_oracle_contract.set_price_data(eth(), Price { + multiplier: 10000, + decimals: 22, + }) + ).assert_success(); + call!( + root, + price_oracle_contract.set_price_data(near(), Price { + multiplier: 10000, + decimals: 28, + }) + ).assert_success(); + + call!( + owner, + pool.register_degen_oracle_config(DegenOracleConfig::PriceOracle(PriceOracleConfig { + oracle_id: price_oracle(), + expire_ts: 3600 * 10u64.pow(9), + maximum_recency_duration_sec: 90, + maximum_staleness_duration_sec: 90 + })), + deposit = 1 + ) + .assert_success(); + call!( + owner, + pool.register_degen_token(to_va(eth()), DegenType::PriceOracle { decimals: 18 }), + deposit = 1 + ) + .assert_success(); + call!( + owner, + pool.register_degen_token(to_va(near()), DegenType::PriceOracle { decimals: 24 }), + deposit = 1 + ) + .assert_success(); + + call!( + root, + pool.update_degen_token_price(to_va(eth())), + deposit = 0 + ) + .assert_success(); + + call!( + root, + pool.update_degen_token_price(to_va(near())), + deposit = 0 + ) + .assert_success(); + + println!("{:?}", view!(pool.get_pool_limit_paged(None, None)).unwrap_json::>()); + + call!( + owner, + pool.add_degen_pool_limit(0, DegenPoolLimitInfo{ + tvl_limit: 10u128.pow(4) * 2 + }), + deposit = 1 + ).assert_success(); + + let out_come = call!( + root, + pool.add_stable_liquidity(0, vec![10000*ONE_ETH, 10000*ONE_NEAR].into_iter().map(|x| U128(x)).collect(), U128(1)), + deposit = to_yocto("0.0007") + ); + out_come.assert_success(); + println!("{:#?}", get_logs(&out_come)); + + let outcome = call!( + root, + pool.add_stable_liquidity(0, vec![10000*ONE_ETH, 10000*ONE_NEAR].into_iter().map(|x| U128(x)).collect(), U128(1)), + deposit = to_yocto("0.0007") + ); + let exe_status = format!("{:?}", outcome.promise_errors()[0].as_ref().unwrap().status()); + assert!(exe_status.contains("Exceed Max TVL")); + + println!("{:?}", view!(pool.get_degen_pool_tvl(0)).unwrap_json::(),); + println!("{:?}", view!(pool.get_pool_limit_by_pool_id(0)).unwrap_json::>()); + call!( + owner, + pool.update_degen_pool_limit(0, DegenPoolLimitInfo{ + tvl_limit: 10u128.pow(4) * 5 + }), + deposit = 1 + ).assert_success(); + println!("{:?}", view!(pool.get_pool_limit_paged(None, None)).unwrap_json::>()); + call!( + owner, + pool.remove_pool_limit(0), + deposit = 1 + ).assert_success(); + println!("{:?}", view!(pool.get_pool_limit_paged(None, None)).unwrap_json::>()); + + let out_come = call!( + root, + pool.add_stable_liquidity(0, vec![10000*ONE_ETH, 10000*ONE_NEAR].into_iter().map(|x| U128(x)).collect(), U128(1)), + deposit = to_yocto("0.0007") + ); + out_come.assert_success(); + println!("{:#?}", get_logs(&out_come)); +} diff --git a/ref-exchange/tests/test_migrate.rs b/ref-exchange/tests/test_migrate.rs index 8d21800..c364186 100644 --- a/ref-exchange/tests/test_migrate.rs +++ b/ref-exchange/tests/test_migrate.rs @@ -51,7 +51,7 @@ fn test_upgrade() { .assert_success(); let metadata = get_metadata(&pool); // println!("{:#?}", metadata); - assert_eq!(metadata.version, "1.9.3".to_string()); + assert_eq!(metadata.version, "1.9.4".to_string()); assert_eq!(metadata.admin_fee_bps, 5); assert_eq!(metadata.boost_farm_id, "boost_farm".to_string()); assert_eq!(metadata.burrowland_id, "burrowland".to_string()); diff --git a/ref-exchange/tests/test_swap.rs b/ref-exchange/tests/test_swap.rs index 56db636..c4b8fb5 100644 --- a/ref-exchange/tests/test_swap.rs +++ b/ref-exchange/tests/test_swap.rs @@ -9,7 +9,7 @@ use near_sdk_sim::{ call, deploy, init_simulator, to_yocto, view, ContractAccount, ExecutionResult, UserAccount, }; -use ref_exchange::{ContractContract as Exchange, PoolInfo, SwapAction}; +use ref_exchange::{Action, ContractContract as Exchange, PoolInfo, SwapAction}; use test_token::ContractContract as TestToken; use mock_wnear::ContractContract as MockWnear; @@ -786,3 +786,131 @@ fn test_direct_swap_wnear_by_output() { let exe_status = format!("{:?}", outcome.promise_errors()[0].as_ref().unwrap().status()); assert!(exe_status.contains("E77: all action types must be the same")); } + +#[test] +fn test_execute_actions_in_va() { + const ONE_USDT: u128 = 10u128.pow(6); + + let root = init_simulator(None); + let owner = root.create_user("owner".to_string(), to_yocto("100")); + let pool = deploy!( + contract: Exchange, + contract_id: swap(), + bytes: &EXCHANGE_WASM_BYTES, + signer_account: root, + init_method: new(to_va("owner".to_string()), to_va("boost_farm".to_string()), to_va("burrowland".to_string()), 30, 0) + ); + call!( + owner, + pool.modify_wnear_id(wnear()), + deposit = 1 + ) + .assert_success(); + let token0 = deploy!( + contract: TestToken, + contract_id: to_va(eth()), + bytes: &TEST_TOKEN_WASM_BYTES, + signer_account: root + ); + call!(root, token0.new()).assert_success(); + call!( + root, + token0.mint(to_va(root.account_id.clone()), to_yocto("10000000").into()) + ) + .assert_success(); + call!( + root, + token0.storage_deposit(Some(to_va(swap())), None), + deposit = to_yocto("1") + ) + .assert_success(); + + + let token1 = deploy!( + contract: TestToken, + contract_id: usdt(), + bytes: &TEST_TOKEN_WASM_BYTES, + signer_account: root + ); + call!(root, token1.new()).assert_success(); + call!( + root, + token1.mint(to_va(root.account_id.clone()), to_yocto("10000000").into()) + ) + .assert_success(); + call!( + root, + token1.storage_deposit(Some(to_va(swap())), None), + deposit = to_yocto("1") + ) + .assert_success(); + + call!( + owner, + pool.extend_whitelisted_tokens(vec![to_va(eth()), to_va(usdt())]), + deposit=1 + ); + + call!( + root, + pool.add_simple_pool(vec![to_va(eth()), to_va(usdt())], 25), + deposit = to_yocto("1") + ) + .assert_success(); + + call!( + root, + pool.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + + call!( + owner, + pool.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + + call!( + root, + token0.ft_transfer_call(to_va(swap()), (144459999999687970893 * 10).into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + call!( + root, + token1.ft_transfer_call(to_va(swap()), (500007198063 * 10).into(), None, "".to_string()), + deposit = 1 + ) + .assert_success(); + + + let out_come = call!( + root, + pool.add_liquidity(0, vec![144459999999687970893, 500007198063].into_iter().map(|x| U128(x)).collect(), Some(vec![U128(1), U128(1)])), + deposit = to_yocto("0.0007") + ); + out_come.assert_success(); + println!("{:#?}", get_logs(&out_come)); + + let max_use_tokens = HashMap::from([(usdt(), U128(ONE_USDT * 4000))]); + let out_come = call!( + root, + pool.execute_actions_in_va( + max_use_tokens, + vec![Action::Swap(SwapAction { + pool_id: 0, + token_in: usdt(), + amount_in: Some(U128(ONE_USDT * 3000)), + token_out: eth(), + min_amount_out: U128(1) + })], + None + ), + gas = 300000000000000 + ); + out_come.assert_success(); + println!("{:#?}", get_logs(&out_come)); + println!("{:#?}", out_come.unwrap_json::>()); +} diff --git a/releases/ref_exchange_release.wasm b/releases/ref_exchange_release.wasm index 67f159c..28100ae 100644 Binary files a/releases/ref_exchange_release.wasm and b/releases/ref_exchange_release.wasm differ