diff --git a/Cargo.lock b/Cargo.lock index 34f61f3..158be3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "ref-exchange" -version = "1.9.6" +version = "1.9.7" dependencies = [ "hex", "mock-boost-farming", diff --git a/ref-exchange/Cargo.toml b/ref-exchange/Cargo.toml index 66bf8af..8f456de 100644 --- a/ref-exchange/Cargo.toml +++ b/ref-exchange/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ref-exchange" -version = "1.9.6" +version = "1.9.7" authors = ["Illia Polosukhin "] edition = "2018" publish = false diff --git a/ref-exchange/release_notes.md b/ref-exchange/release_notes.md index a9a7d6d..d8599a0 100644 --- a/ref-exchange/release_notes.md +++ b/ref-exchange/release_notes.md @@ -1,5 +1,13 @@ # Release Notes +### Version 1.9.7 +``` +CMN4goNWHQjsXevLbqAC9nXKTw1yeJqysEfB647uuyro +``` +1. fix identity verification for the whitelisted_postfix related functions. +2. add donation functions. +3. add mft_unregister. + ### Version 1.9.6 ``` 2Yo8qJ5S3biFJbnBdb4ZNcGhN1RhHJq34cGF4g7Yigcw diff --git a/ref-exchange/src/degen_swap/mod.rs b/ref-exchange/src/degen_swap/mod.rs index 35d8fac..0c8a45c 100644 --- a/ref-exchange/src/degen_swap/mod.rs +++ b/ref-exchange/src/degen_swap/mod.rs @@ -717,6 +717,13 @@ impl DegenSwapPool { self.shares.insert(account_id, &0); } + /// Unregister account with shares balance of 0. + /// The storage should be refunded to the user. + pub fn share_unregister(&mut self, account_id: &AccountId) { + let shares = self.shares.remove(account_id); + assert!(shares.expect(ERR13_LP_NOT_REGISTERED) == 0, "{}", ERR19_NONZERO_LP_SHARES); + } + /// Transfers shares from predecessor to receiver. pub fn share_transfer(&mut self, sender_id: &AccountId, receiver_id: &AccountId, amount: u128) { let balance = self.shares.get(&sender_id).expect(ERR13_LP_NOT_REGISTERED); diff --git a/ref-exchange/src/donation.rs b/ref-exchange/src/donation.rs new file mode 100644 index 0000000..8c8193e --- /dev/null +++ b/ref-exchange/src/donation.rs @@ -0,0 +1,65 @@ +use crate::*; + +#[near_bindgen] +impl Contract { + /// The user donates the shares they hold to the protocol. + /// + /// # Arguments + /// + /// * `pool_id` - The pool id where the shares are located. + /// * `amount` - The donation amount; if it's None, the entire amount will be donated. + /// * `unregister` - If `Some(true)`, Will attempt to unregister the shares and refund the user’s storage fees. + /// The storage fee will be refunded to the user's internal account first; + /// if there is no internal account, a transfer will be initiated. + #[payable] + pub fn donation_share(&mut self, pool_id: u64, amount: Option, unregister: Option) { + assert_one_yocto(); + self.assert_contract_running(); + let account_id = env::predecessor_account_id(); + let prev_storage = env::storage_usage(); + let mut pool = self.pools.get(pool_id).expect(ERR85_NO_POOL); + let donation_amount = amount.map(|v| v.0).unwrap_or(pool.share_balances(&account_id)); + assert!(donation_amount > 0, "Invalid amount"); + pool.share_transfer(&account_id, &env::current_account_id(), donation_amount); + if unregister == Some(true) { + pool.share_unregister(&account_id); + } + self.pools.replace(pool_id, &pool); + if prev_storage > env::storage_usage() { + let refund = (prev_storage - env::storage_usage()) as Balance * env::storage_byte_cost(); + if let Some(mut account) = self.internal_get_account(&account_id) { + account.near_amount += refund; + self.internal_save_account(&account_id, account); + } else { + Promise::new(account_id.clone()).transfer(refund); + } + } + event::Event::DonationShare { account_id: &account_id, pool_id, amount: U128(donation_amount) }.emit(); + } + + /// The user donates the tokens they hold to the owner. + /// + /// # Arguments + /// + /// * `token_id` - The id of the donated token. + /// * `amount` - The donation amount; if it's None, the entire amount will be donated. + /// * `unregister` - If `Some(true)`, Will attempt to unregister the tokens. + #[payable] + pub fn donation_token(&mut self, token_id: ValidAccountId, amount: Option, unregister: Option) { + assert_one_yocto(); + self.assert_contract_running(); + let account_id = env::predecessor_account_id(); + let mut account = self.internal_unwrap_account(&account_id); + let donation_amount = amount.map(|v| v.0).unwrap_or(account.get_balance(token_id.as_ref()).expect("Invalid token_id")); + assert!(donation_amount > 0, "Invalid amount"); + account.withdraw(token_id.as_ref(), donation_amount); + if unregister == Some(true) { + account.unregister(token_id.as_ref()); + } + self.internal_save_account(&account_id, account); + let mut owner_account = self.internal_unwrap_account(&self.owner_id); + owner_account.deposit(token_id.as_ref(), donation_amount); + self.accounts.insert(&self.owner_id, &owner_account.into()); + event::Event::DonationToken { account_id: &account_id, token_id: token_id.as_ref(), amount: U128(donation_amount) }.emit(); + } +} diff --git a/ref-exchange/src/errors.rs b/ref-exchange/src/errors.rs index 0a7f354..f970e78 100644 --- a/ref-exchange/src/errors.rs +++ b/ref-exchange/src/errors.rs @@ -9,6 +9,7 @@ pub const ERR15_NO_STORAGE_CAN_WITHDRAW: &str = "E15: no storage can withdraw"; pub const ERR16_STORAGE_WITHDRAW_TOO_MUCH: &str = "E16: storage withdraw too much"; pub const ERR17_DEPOSIT_LESS_THAN_MIN_STORAGE: &str = "E17: deposit less than min storage"; pub const ERR18_TOKENS_NOT_EMPTY: &str = "E18: storage unregister tokens not empty"; +pub const ERR19_NONZERO_LP_SHARES: &str = "E19: Nonzero LP shares"; // Accounts. @@ -90,6 +91,7 @@ pub const ERR105_WHITELISTED_POSTFIX_NOT_IN_LIST: &str = "E105: whitelisted post //mft pub const ERR110_INVALID_REGISTER: &str = "E110: Invalid register"; +pub const ERR111_INVALID_UNREGISTER: &str = "E111: Invalid unregister"; // rated pool pub const ERR120_RATES_EXPIRED: &str = "E120: Rates expired"; diff --git a/ref-exchange/src/event.rs b/ref-exchange/src/event.rs new file mode 100644 index 0000000..3a5f7a3 --- /dev/null +++ b/ref-exchange/src/event.rs @@ -0,0 +1,38 @@ + +use crate::*; +use near_sdk::serde_json::json; + +const EVENT_STANDARD: &str = "exchange.ref"; +const EVENT_STANDARD_VERSION: &str = "1.0.0"; + +#[derive(Serialize, Debug, Clone)] +#[serde(crate = "near_sdk::serde")] +#[serde(tag = "event", content = "data")] +#[serde(rename_all = "snake_case")] +#[must_use = "Don't forget to `.emit()` this event"] +pub enum Event<'a> { + DonationShare { + account_id: &'a AccountId, + pool_id: u64, + amount: U128, + }, + DonationToken { + account_id: &'a AccountId, + token_id: &'a AccountId, + amount: U128, + } +} + +impl Event<'_> { + pub fn emit(&self) { + let data = json!(self); + let event_json = json!({ + "standard": EVENT_STANDARD, + "version": EVENT_STANDARD_VERSION, + "event": data["event"], + "data": [data["data"]] + }) + .to_string(); + log!("EVENT_JSON:{}", event_json); + } +} \ No newline at end of file diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index 0a5b83f..0074adc 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -58,6 +58,8 @@ mod shadow_actions; mod unit_lpt_cumulative_infos; mod pool_limit_info; mod client_echo_limit; +mod donation; +mod event; near_sdk::setup_alloc!(); diff --git a/ref-exchange/src/multi_fungible_token.rs b/ref-exchange/src/multi_fungible_token.rs index 4bb0631..c1fb411 100644 --- a/ref-exchange/src/multi_fungible_token.rs +++ b/ref-exchange/src/multi_fungible_token.rs @@ -185,6 +185,27 @@ impl Contract { } } + /// Unregister LP token of given pool for given account. + #[payable] + pub fn mft_unregister(&mut self, token_id: String) { + assert_one_yocto(); + self.assert_contract_running(); + let account_id = env::predecessor_account_id(); + let prev_storage = env::storage_usage(); + match parse_token_id(token_id) { + TokenOrPool::Token(_) => env::panic(ERR111_INVALID_UNREGISTER.as_bytes()), + TokenOrPool::Pool(pool_id) => { + let mut pool = self.pools.get(pool_id).expect(ERR85_NO_POOL); + pool.share_unregister(&account_id); + self.pools.replace(pool_id, &pool); + if prev_storage > env::storage_usage() { + let refund = (prev_storage - env::storage_usage()) as Balance * env::storage_byte_cost(); + Promise::new(account_id).transfer(refund); + } + } + } + } + /// Transfer one of internal tokens: LP or balances. /// `token_id` can either by account of the token or pool number. #[payable] diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index a3b5fe9..a448e96 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -75,7 +75,7 @@ impl Contract { #[payable] pub fn extend_auto_whitelisted_postfix(&mut self, postfixes: Vec) { assert_one_yocto(); - self.is_owner_or_guardians(); + assert!(self.is_owner_or_guardians(), "{}", ERR100_NOT_ALLOWED); for postfix in postfixes { self.auto_whitelisted_postfix.insert(postfix.clone()); } @@ -84,7 +84,7 @@ impl Contract { #[payable] pub fn remove_auto_whitelisted_postfix(&mut self, postfixes: Vec) { assert_one_yocto(); - self.is_owner_or_guardians(); + assert!(self.is_owner_or_guardians(), "{}", ERR100_NOT_ALLOWED); for postfix in postfixes { let exist = self.auto_whitelisted_postfix.remove(&postfix); assert!(exist, "{}", ERR105_WHITELISTED_POSTFIX_NOT_IN_LIST); @@ -300,9 +300,8 @@ impl Contract { assert!(amount > 0, "{}", ERR29_ILLEGAL_WITHDRAW_AMOUNT); let owner_id = self.owner_id.clone(); let mut account = self.internal_unwrap_account(&owner_id); - // Note: subtraction and deregistration will be reverted if the promise fails. account.withdraw(&token_id, amount); - self.internal_save_account(&owner_id, account); + self.accounts.insert(&owner_id, &account.into()); self.internal_send_tokens(&owner_id, &token_id, amount, skip_unwrap_near) } diff --git a/ref-exchange/src/pool.rs b/ref-exchange/src/pool.rs index 0596f21..0432f6b 100644 --- a/ref-exchange/src/pool.rs +++ b/ref-exchange/src/pool.rs @@ -275,6 +275,15 @@ impl Pool { } } + pub fn share_unregister(&mut self, account_id: &AccountId) { + match self { + Pool::SimplePool(pool) => pool.share_unregister(account_id), + Pool::StableSwapPool(pool) => pool.share_unregister(account_id), + Pool::RatedSwapPool(pool) => pool.share_unregister(account_id), + Pool::DegenSwapPool(pool) => pool.share_unregister(account_id), + } + } + pub fn predict_add_rated_liquidity( &self, amounts: &Vec, diff --git a/ref-exchange/src/rated_swap/mod.rs b/ref-exchange/src/rated_swap/mod.rs index f1f46a7..358f7fc 100644 --- a/ref-exchange/src/rated_swap/mod.rs +++ b/ref-exchange/src/rated_swap/mod.rs @@ -715,6 +715,13 @@ impl RatedSwapPool { self.shares.insert(account_id, &0); } + /// Unregister account with shares balance of 0. + /// The storage should be refunded to the user. + pub fn share_unregister(&mut self, account_id: &AccountId) { + let shares = self.shares.remove(account_id); + assert!(shares.expect(ERR13_LP_NOT_REGISTERED) == 0, "{}", ERR19_NONZERO_LP_SHARES); + } + /// Transfers shares from predecessor to receiver. pub fn share_transfer(&mut self, sender_id: &AccountId, receiver_id: &AccountId, amount: u128) { let balance = self.shares.get(&sender_id).expect(ERR13_LP_NOT_REGISTERED); diff --git a/ref-exchange/src/simple_pool.rs b/ref-exchange/src/simple_pool.rs index 64d1972..6162ef7 100644 --- a/ref-exchange/src/simple_pool.rs +++ b/ref-exchange/src/simple_pool.rs @@ -82,6 +82,13 @@ impl SimplePool { self.shares.insert(account_id, &0); } + /// Unregister account with shares balance of 0. + /// The storage should be refunded to the user. + pub fn share_unregister(&mut self, account_id: &AccountId) { + let shares = self.shares.remove(account_id); + assert!(shares.expect(ERR13_LP_NOT_REGISTERED) == 0, "{}", ERR19_NONZERO_LP_SHARES); + } + /// Transfers shares from predecessor to receiver. pub fn share_transfer(&mut self, sender_id: &AccountId, receiver_id: &AccountId, amount: u128) { let balance = self.shares.get(&sender_id).expect(ERR13_LP_NOT_REGISTERED); diff --git a/ref-exchange/src/stable_swap/mod.rs b/ref-exchange/src/stable_swap/mod.rs index 593a7cf..e465898 100644 --- a/ref-exchange/src/stable_swap/mod.rs +++ b/ref-exchange/src/stable_swap/mod.rs @@ -625,6 +625,13 @@ impl StableSwapPool { self.shares.insert(account_id, &0); } + /// Unregister account with shares balance of 0. + /// The storage should be refunded to the user. + pub fn share_unregister(&mut self, account_id: &AccountId) { + let shares = self.shares.remove(account_id); + assert!(shares.expect(ERR13_LP_NOT_REGISTERED) == 0, "{}", ERR19_NONZERO_LP_SHARES); + } + /// Transfers shares from predecessor to receiver. pub fn share_transfer(&mut self, sender_id: &AccountId, receiver_id: &AccountId, amount: u128) { let balance = self.shares.get(&sender_id).expect(ERR13_LP_NOT_REGISTERED); diff --git a/ref-exchange/tests/test_donation.rs b/ref-exchange/tests/test_donation.rs new file mode 100644 index 0000000..ba2ac52 --- /dev/null +++ b/ref-exchange/tests/test_donation.rs @@ -0,0 +1,138 @@ +use std::collections::HashMap; +use near_sdk::json_types::U128; +use near_sdk::AccountId; +use near_sdk_sim::{call, to_yocto, view}; + +use crate::common::utils::*; +pub mod common; + +#[test] +fn donation_share() { + let (root, _owner, pool, _token1, _token2, _token3) = setup_pool_with_liquidity(); + assert_eq!(mft_balance_of(&pool, ":0", &root.account_id()), to_yocto("1")); + assert_eq!(mft_balance_of(&pool, ":0", &pool.account_id()), 0); + assert_eq!(mft_total_supply(&pool, ":0"), to_yocto("1")); + let deposit_before_donation_share = get_storage_state(&pool, to_va(root.account_id.clone())).unwrap().deposit; + call!( + root, + pool.donation_share( + 0, None, None + ), + deposit = 1 + ).assert_success(); + assert_eq!(deposit_before_donation_share, get_storage_state(&pool, to_va(root.account_id.clone())).unwrap().deposit); + assert_eq!(mft_balance_of(&pool, ":0", &root.account_id()), 0); + assert_eq!(mft_balance_of(&pool, ":0", &pool.account_id()), to_yocto("1")); + assert_eq!(mft_total_supply(&pool, ":0"), to_yocto("1")); + + assert!(mft_has_registered(&pool, ":0", root.valid_account_id())); + call!( + root, + pool.mft_unregister(":0".to_string()), + deposit = 1 + ).assert_success(); + assert!(!mft_has_registered(&pool, ":0", root.valid_account_id())); + + call!( + root, + pool.add_liquidity(0, vec![U128(to_yocto("10")), U128(to_yocto("20"))], None), + deposit = to_yocto("0.0007") + ) + .assert_success(); + assert_eq!(mft_balance_of(&pool, ":0", &root.account_id()), 999999999999999999999999); + assert_eq!(mft_balance_of(&pool, ":0", &pool.account_id()), to_yocto("1")); + assert_eq!(mft_total_supply(&pool, ":0"), 1999999999999999999999999u128); + call!( + root, + pool.donation_share( + 0, Some(U128(1)), None + ), + deposit = 1 + ).assert_success(); + assert_eq!(mft_balance_of(&pool, ":0", &root.account_id()), 999999999999999999999998); + assert_eq!(mft_balance_of(&pool, ":0", &pool.account_id()), to_yocto("1") + 1); + assert_eq!(mft_total_supply(&pool, ":0"), 1999999999999999999999999u128); + let deposit_before_donation_share = get_storage_state(&pool, to_va(root.account_id.clone())).unwrap().deposit.0; + let outcome = call!( + root, + pool.donation_share( + 0, None, Some(true) + ), + deposit = 1 + ); + println!("{:#?}", get_logs(&outcome)); + + assert_eq!(mft_balance_of(&pool, ":0", &root.account_id()), 0); + assert_eq!(mft_balance_of(&pool, ":0", &pool.account_id()), 1999999999999999999999999u128); + assert_eq!(mft_total_supply(&pool, ":0"), 1999999999999999999999999u128); + assert!(deposit_before_donation_share < get_storage_state(&pool, to_va(root.account_id.clone())).unwrap().deposit.0); +} + +#[test] +fn donation_token() { + let (root, owner, pool, token1, _token2, _token3) = setup_pool_with_liquidity(); + let user = root.create_user("user".to_string(), to_yocto("100")); + mint_and_deposit_token(&user, &token1, &pool, to_yocto("100")); + let balances = view!(pool.get_deposits(to_va(user.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("100")); + let balances = view!(pool.get_deposits(to_va(owner.account_id.clone()))) + .unwrap_json::>(); + assert!(balances.is_empty()); + call!( + user, + pool.donation_token( + token1.valid_account_id(), None, None + ), + deposit = 1 + ).assert_success(); + let balances = view!(pool.get_deposits(to_va(user.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, 0); + let balances = view!(pool.get_deposits(to_va(owner.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("100")); + + let user1 = root.create_user("user1".to_string(), to_yocto("500")); + mint_and_deposit_token(&user1, &token1, &pool, to_yocto("500")); + let balances = view!(pool.get_deposits(to_va(user1.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("500")); + call!( + user1, + pool.donation_token( + token1.valid_account_id(), Some(U128(to_yocto("100"))), None + ), + deposit = 1 + ).assert_success(); + let balances = view!(pool.get_deposits(to_va(user1.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("400")); + let balances = view!(pool.get_deposits(to_va(owner.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("200")); + + let outcome = call!( + user1, + pool.donation_token( + token1.valid_account_id(), None, Some(true) + ), + deposit = 1 + ); + println!("{:#?}", get_logs(&outcome)); + + let balances = view!(pool.get_deposits(to_va(user1.account_id.clone()))) + .unwrap_json::>(); + assert!(balances.is_empty()); + let balances = view!(pool.get_deposits(to_va(owner.account_id.clone()))) + .unwrap_json::>(); + let balances = balances.get(&token1.account_id()).unwrap().0; + assert_eq!(balances, to_yocto("600")); + +} \ No newline at end of file diff --git a/ref-exchange/tests/test_migrate.rs b/ref-exchange/tests/test_migrate.rs index 1794d0f..b534718 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.6".to_string()); + assert_eq!(metadata.version, "1.9.7".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/releases/ref_exchange_release.wasm b/releases/ref_exchange_release.wasm index 489dfe8..7a2ca09 100644 Binary files a/releases/ref_exchange_release.wasm and b/releases/ref_exchange_release.wasm differ diff --git a/releases/ref_exchange_release_v196.wasm b/releases/ref_exchange_release_v196.wasm new file mode 100644 index 0000000..489dfe8 Binary files /dev/null and b/releases/ref_exchange_release_v196.wasm differ