diff --git a/ref-exchange/release_notes.md b/ref-exchange/release_notes.md index 64cae43..62efeaf 100644 --- a/ref-exchange/release_notes.md +++ b/ref-exchange/release_notes.md @@ -2,9 +2,11 @@ ### Version 1.9.7 ``` -Cc7rca9tb9CZKQRkm5fqXMErVuCcGr3aeoxNsUgmkVhX +B2646W6czcBc6AnZfb4uETrAfcptNicVuYeiraoZFKA8 ``` -1. fix identity verification for the whitelisted_postfix related interfaces. +1. fix identity verification for the whitelisted_postfix related functions. +2. add donation functions. +3. add mft_unregister. ### Version 1.9.6 ``` diff --git a/ref-exchange/src/degen_swap/mod.rs b/ref-exchange/src/degen_swap/mod.rs index 35d8fac..6c1011b 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_LP_NOT_EMPTY); + } + /// 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..6a89d9b --- /dev/null +++ b/ref-exchange/src/donation.rs @@ -0,0 +1,47 @@ +use crate::*; + +#[near_bindgen] +impl Contract { + #[payable] + pub fn donation_share(&mut self, pool_id: u64, amount: Option, unregister: Option) { + assert_one_yocto(); + 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(); + } + + #[payable] + pub fn donation_token(&mut self, token_id: ValidAccountId, amount: Option, unregister: Option) { + assert_one_yocto(); + 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..5dd65dc 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_LP_NOT_EMPTY: &str = "E19: LP not empty"; // 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..f259e56 100644 --- a/ref-exchange/src/multi_fungible_token.rs +++ b/ref-exchange/src/multi_fungible_token.rs @@ -185,6 +185,26 @@ impl Contract { } } + /// Unregister LP token of given pool for given account. + #[payable] + pub fn mft_unregister(&mut self, token_id: String) { + assert_one_yocto(); + 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 d234485..3838925 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -302,7 +302,7 @@ impl Contract { 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..9a00ceb 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_LP_NOT_EMPTY); + } + /// 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..dbbab34 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_LP_NOT_EMPTY); + } + /// 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..ff5c5d4 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_LP_NOT_EMPTY); + } + /// 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/releases/ref_exchange_release.wasm b/releases/ref_exchange_release.wasm index d9fd3ab..48728ad 100644 Binary files a/releases/ref_exchange_release.wasm and b/releases/ref_exchange_release.wasm differ