From e23a40ec122b6abc16be1f78f61572fe3c006124 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 9 Apr 2021 17:04:52 +0200 Subject: [PATCH 1/6] near and storage tracing --- ref-exchange/src/account_deposit.rs | 118 ++++++++++++++++++---------- ref-exchange/src/errors.rs | 2 +- ref-exchange/src/lib.rs | 51 ++++++------ ref-exchange/src/owner.rs | 2 +- ref-exchange/src/storage_impl.rs | 20 ++--- ref-exchange/src/views.rs | 6 +- res/ref_exchange_local.wasm | Bin 338262 -> 338573 bytes 7 files changed, 119 insertions(+), 80 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index 66c07e7..e51e816 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -5,29 +5,52 @@ use std::convert::TryInto; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::json_types::{ValidAccountId, U128}; -use near_sdk::{assert_one_yocto, env, near_bindgen, AccountId, Balance, PromiseResult}; +use near_sdk::{ + assert_one_yocto, env, near_bindgen, AccountId, Balance, PromiseResult, StorageUsage, +}; use crate::utils::{ext_fungible_token, ext_self, GAS_FOR_FT_TRANSFER}; use crate::*; -const MAX_ACCOUNT_LENGTH: u128 = 64; -const MIN_ACCOUNT_DEPOSIT_LENGTH: u128 = MAX_ACCOUNT_LENGTH + 16 + 4; +/// bytes length of u64 values +const U64_STORAGE: StorageUsage = 8; +/// bytes length of u32 values. Used in length operations +const U32_STORAGE: StorageUsage = 4; +// 64 = max account name length +pub const INIT_ACCOUNT_STORAGE: StorageUsage = 64 + 2 * U64_STORAGE + U32_STORAGE; + +// NEAR native token. This is not a valid token ID. HACK: NEAR is a native token, we use the +// empty string we use it to reference not existing near account. +// pub const NEAR: AccountId = "".to_string(); /// Account deposits information and storage cost. #[derive(BorshSerialize, BorshDeserialize, Default)] #[cfg_attr(feature = "test", derive(Clone))] pub struct AccountDeposit { - /// Native amount sent to the exchange. - /// Used for storage now, but in future can be used for trading as well. - pub amount: Balance, + /// NEAR sent to the exchange. + /// Used for storage and trading. + pub near_amount: Balance, /// Amounts of various tokens in this account. pub tokens: HashMap, + pub storage_used: StorageUsage, } impl AccountDeposit { + pub fn new(account_id: &AccountId, near_amount: Balance) -> Self { + Self { + near_amount, + tokens: HashMap::default(), + // Here we manually compute the initial storage size of account deposit. + storage_used: account_id.len() as StorageUsage + U64_STORAGE + U32_STORAGE, + } + } + /// Adds amount to the balance of given token while checking that storage is covered. pub(crate) fn add(&mut self, token: &AccountId, amount: Balance) { - if let Some(x) = self.tokens.get_mut(token) { + if *token == "" { + // We use empty string to represent NEAR + self.near_amount += amount; + } else if let Some(x) = self.tokens.get_mut(token) { *x = *x + amount; } else { self.tokens.insert(token.clone(), amount); @@ -38,38 +61,52 @@ impl AccountDeposit { /// Subtract from `token` balance. /// Panics if `amount` is bigger than the current balance. pub(crate) fn sub(&mut self, token: &AccountId, amount: Balance) { + if *token == "" { + // We use empty string to represent NEAR + self.near_amount -= amount; + self.assert_storage_usage(); + return; + } let value = *self.tokens.get(token).expect(ERR21_TOKEN_NOT_REG); assert!(value >= amount, "{}", ERR22_NOT_ENOUGH_TOKENS); self.tokens.insert(token.clone(), value - amount); } - /// Returns amount of $NEAR necessary to cover storage used by this data structure. + /// Returns amount of $NEAR necessary to cover storage used by account referenced to this structure. + #[inline] pub fn storage_usage(&self) -> Balance { - (MIN_ACCOUNT_DEPOSIT_LENGTH + self.tokens.len() as u128 * (MAX_ACCOUNT_LENGTH + 16)) + (if self.storage_used < INIT_ACCOUNT_STORAGE { + self.storage_used + } else { + INIT_ACCOUNT_STORAGE + }) as Balance * env::storage_byte_cost() } /// Returns how much NEAR is available for storage. - pub fn storage_available(&self) -> Balance { - self.amount - self.storage_usage() + #[inline] + pub(crate) fn storage_available(&self) -> Balance { + self.near_amount - self.storage_usage() } /// Asserts there is sufficient amount of $NEAR to cover storage usage. pub fn assert_storage_usage(&self) { assert!( - self.storage_usage() <= self.amount, + self.storage_usage() <= self.near_amount, "{}", ERR11_INSUFFICIENT_STORAGE ); } - /// Returns minimal account deposit storage usage possible. - pub fn min_storage_usage() -> Balance { - MIN_ACCOUNT_DEPOSIT_LENGTH * env::storage_byte_cost() + /// Updates the account storage usage and sets. + pub(crate) fn update_storage(&mut self, tx_start_storage: StorageUsage) { + let s = env::storage_usage(); + self.storage_used += s - tx_start_storage; + self.assert_storage_usage(); } - /// Registers given token and set balance to 0. - /// Fails if not enough amount to cover new storage usage. + /// Registers given `token_id` and set balance to 0. + /// Fails if not enough NEAR is in deposit to cover new storage usage. pub(crate) fn register(&mut self, token_ids: &Vec) { for token_id in token_ids { let t = token_id.as_ref(); @@ -94,20 +131,20 @@ impl Contract { /// Fails if not enough balance on this account to cover storage. pub fn register_tokens(&mut self, token_ids: Vec) { let sender_id = env::predecessor_account_id(); - let mut deposits = self.get_account_deposits(&sender_id); + let mut deposits = self.get_account(&sender_id); deposits.register(&token_ids); - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits); } /// Unregister given token from user's account deposit. /// Panics if the balance of any given token is non 0. pub fn unregister_tokens(&mut self, token_ids: Vec) { let sender_id = env::predecessor_account_id(); - let mut deposits = self.get_account_deposits(&sender_id); + let mut deposits = self.get_account(&sender_id); for token_id in token_ids { deposits.unregister(token_id.as_ref()); } - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits); } /// Withdraws given token from the deposits of given user. @@ -119,13 +156,13 @@ impl Contract { let token_id: AccountId = token_id.into(); let amount: u128 = amount.into(); let sender_id = env::predecessor_account_id(); - let mut deposits = self.get_account_deposits(&sender_id); + let mut deposits = self.get_account(&sender_id); // Note: subtraction and deregistration will be reverted if the promise fails. deposits.sub(&token_id, amount); if unregister == Some(true) { deposits.unregister(&token_id); } - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits); ext_fungible_token::ft_transfer( sender_id.clone().try_into().unwrap(), amount.into(), @@ -162,9 +199,9 @@ impl Contract { PromiseResult::Successful(_) => {} PromiseResult::Failed => { // This reverts the changes from withdraw function. - let mut deposits = self.get_account_deposits(&sender_id); + let mut deposits = self.get_account(&sender_id); deposits.add(&token_id, amount.0); - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits); } }; } @@ -174,10 +211,14 @@ impl Contract { /// Registers account in deposited amounts with given amount of $NEAR. /// If account already exists, adds amount to it. /// This should be used when it's known that storage is prepaid. - pub(crate) fn internal_register_account(&mut self, account_id: &AccountId, amount: Balance) { - let mut deposit_amount = self.deposited_amounts.get(&account_id).unwrap_or_default(); - deposit_amount.amount += amount; - self.deposited_amounts.insert(&account_id, &deposit_amount); + pub(crate) fn register_account(&mut self, account_id: &AccountId, amount: Balance) { + let acc = if let Some(mut account_deposit) = self.accounts.get(&account_id) { + account_deposit.near_amount += amount; + account_deposit + } else { + AccountDeposit::new(account_id, amount) + }; + self.accounts.insert(&account_id, &acc); } /// Record deposit of some number of tokens to this contract. @@ -188,32 +229,29 @@ impl Contract { token_id: &AccountId, amount: Balance, ) { - let mut account_deposit = self.get_account_deposits(sender_id); + let mut acc = self.get_account(sender_id); assert!( - self.whitelisted_tokens.contains(token_id) - || account_deposit.tokens.contains_key(token_id), + self.whitelisted_tokens.contains(token_id) || acc.tokens.contains_key(token_id), "{}", ERR12_TOKEN_NOT_WHITELISTED ); - account_deposit.add(token_id, amount); - self.deposited_amounts.insert(sender_id, &account_deposit); + acc.add(token_id, amount); + self.accounts.insert(sender_id, &acc); } // Returns `from` AccountDeposit. #[inline] - pub(crate) fn get_account_deposits(&self, from: &AccountId) -> AccountDeposit { - self.deposited_amounts - .get(from) - .expect(ERR10_ACC_NOT_REGISTERED) + pub(crate) fn get_account(&self, from: &AccountId) -> AccountDeposit { + self.accounts.get(from).expect(ERR10_ACC_NOT_REGISTERED) } /// Returns current balance of given token for given user. If there is nothing recorded, returns 0. - pub(crate) fn internal_get_deposit( + pub(crate) fn get_deposit_balance( &self, sender_id: &AccountId, token_id: &AccountId, ) -> Balance { - self.deposited_amounts + self.accounts .get(sender_id) .and_then(|d| d.tokens.get(token_id).cloned()) .unwrap_or_default() diff --git a/ref-exchange/src/errors.rs b/ref-exchange/src/errors.rs index bcfa910..9142e0d 100644 --- a/ref-exchange/src/errors.rs +++ b/ref-exchange/src/errors.rs @@ -5,7 +5,7 @@ pub const ERR11_INSUFFICIENT_STORAGE: &str = "E11: insufficient $NEAR storage de pub const ERR12_TOKEN_NOT_WHITELISTED: &str = "E12: token not whitelisted"; // Account Deposits // - +// TODO: change type to avoid assert!(..., "{}", ERR...). Maybe using raw strings? r#""# 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"; diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index c8c5182..b5f3c98 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -8,7 +8,7 @@ use near_sdk::collections::{LookupMap, UnorderedSet, Vector}; use near_sdk::json_types::{ValidAccountId, U128}; use near_sdk::{assert_one_yocto, env, log, near_bindgen, AccountId, PanicOnDefault, Promise}; -use crate::account_deposit::AccountDeposit; +use crate::account_deposit::{AccountDeposit, INIT_ACCOUNT_STORAGE}; pub use crate::action::*; use crate::errors::*; use crate::pool::Pool; @@ -42,7 +42,7 @@ pub struct Contract { /// List of all the pools. pools: Vector, /// Balances of deposited tokens for each account. - deposited_amounts: LookupMap, + accounts: LookupMap, /// Set of whitelisted tokens by "owner". whitelisted_tokens: UnorderedSet, } @@ -56,15 +56,16 @@ impl Contract { exchange_fee, referral_fee, pools: Vector::new(b"p".to_vec()), - deposited_amounts: LookupMap::new(b"d".to_vec()), + accounts: LookupMap::new(b"d".to_vec()), whitelisted_tokens: UnorderedSet::new(b"w".to_vec()), } } /// Adds new "Simple Pool" with given tokens and given fee. - /// Attached NEAR should be enough to cover the added storage. + /// Deposited NEAR must be enough to cover the added storage. #[payable] pub fn add_simple_pool(&mut self, tokens: Vec, fee: u32) -> u64 { + assert_one_yocto(); check_token_duplicates(&tokens); self.internal_add_pool(Pool::SimplePool(SimplePool::new( self.pools.len() as u32, @@ -121,13 +122,13 @@ impl Contract { assert!(amount >= &min_amount.0, "ERR_MIN_AMOUNT"); } } - let mut deposits = self.deposited_amounts.get(&sender_id).unwrap_or_default(); + let mut acc = self.get_account(&sender_id); let tokens = pool.tokens(); // Subtract updated amounts from deposits. This will fail if there is not enough funds for any of the tokens. for i in 0..tokens.len() { - deposits.sub(&tokens[i], amounts[i]); + acc.sub(&tokens[i], amounts[i]); } - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &acc); self.pools.replace(pool_id, &pool); } @@ -147,11 +148,11 @@ impl Contract { ); self.pools.replace(pool_id, &pool); let tokens = pool.tokens(); - let mut deposits = self.deposited_amounts.get(&sender_id).unwrap_or_default(); + let mut acc = self.get_account(&&sender_id); for i in 0..tokens.len() { - deposits.add(&tokens[i], amounts[i]); + acc.add(&tokens[i], amounts[i]); } - self.deposited_amounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &acc); } } @@ -161,20 +162,12 @@ impl Contract { /// If there is not enough attached balance to cover storage, fails. /// If too much attached - refunds it back. fn internal_add_pool(&mut self, pool: Pool) -> u64 { - let prev_storage = env::storage_usage(); + let start_storage = env::storage_usage(); let id = self.pools.len() as u64; self.pools.push(&pool); - // Check how much storage cost and refund the left over back. - let storage_cost = (env::storage_usage() - prev_storage) as u128 * env::storage_byte_cost(); - assert!( - storage_cost <= env::attached_deposit(), - "ERR_STORAGE_DEPOSIT" - ); - let refund = env::attached_deposit() - storage_cost; - if refund > 0 { - Promise::new(env::predecessor_account_id()).transfer(refund); - } + // TODO: update deposit handling + id } @@ -190,8 +183,9 @@ impl Contract { min_amount_out: u128, referral_id: &Option, ) -> u128 { - let mut deposits = self.deposited_amounts.get(&sender_id).unwrap_or_default(); - deposits.sub(token_in, amount_in); + let start_storage = env::storage_usage(); + let mut acc = self.accounts.get(&sender_id).unwrap_or_default(); + acc.sub(token_in, amount_in); let mut pool = self.pools.get(pool_id).expect("ERR_NO_POOL"); let amount_out = pool.swap( token_in, @@ -201,9 +195,16 @@ impl Contract { &self.owner_id, referral_id, ); - deposits.add(token_out, amount_out); - self.deposited_amounts.insert(&sender_id, &deposits); + acc.add(token_out, amount_out); + self.accounts.insert(&sender_id, &acc); self.pools.replace(pool_id, &pool); + // TODO: + // we need to insert 2 times: once in line 194 (prev) and second time here. + // This is because we won't trace properly new storage until we insert the record into the storage tree. + // Alternative would be to refactor the AccountDeposit and move ynear to another, top-level map. + // Better solution: compute the storage consumed by AccountDeposit on the fly. + acc.update_storage(start_storage); + amount_out } } diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index 23f57c3..b982f2f 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -64,7 +64,7 @@ impl Contract { exchange_fee: contract_v1.exchange_fee, referral_fee: contract_v1.referral_fee, pools: contract_v1.pools, - deposited_amounts: contract_v1.deposited_amounts, + accounts: contract_v1.deposited_amounts, whitelisted_tokens: UnorderedSet::new(b"w".to_vec()), } } diff --git a/ref-exchange/src/storage_impl.rs b/ref-exchange/src/storage_impl.rs index cd4b952..45bf223 100644 --- a/ref-exchange/src/storage_impl.rs +++ b/ref-exchange/src/storage_impl.rs @@ -20,20 +20,20 @@ impl StorageManagement for Contract { } if registration_only { // Registration only setups the account but doesn't leave space for tokens. - if self.deposited_amounts.contains_key(&account_id) { + if self.accounts.contains_key(&account_id) { log!("ERR_ACC_REGISTERED"); if amount > 0 { Promise::new(env::predecessor_account_id()).transfer(amount); } } else { - self.internal_register_account(&account_id, min_balance); + self.register_account(&account_id, min_balance); let refund = amount - min_balance; if refund > 0 { Promise::new(env::predecessor_account_id()).transfer(refund); } } } else { - self.internal_register_account(&account_id, amount); + self.register_account(&account_id, amount); } self.storage_balance_of(account_id.try_into().unwrap()) .unwrap() @@ -43,7 +43,7 @@ impl StorageManagement for Contract { fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { assert_one_yocto(); let account_id = env::predecessor_account_id(); - let account_deposit = self.get_account_deposits(&account_id); + let account_deposit = self.get_account(&account_id); let available = account_deposit.storage_available(); let amount = amount.map(|a| a.0).unwrap_or(available); assert!(amount <= available, "ERR_STORAGE_WITHDRAW_TOO_MUCH"); @@ -57,14 +57,14 @@ impl StorageManagement for Contract { fn storage_unregister(&mut self, force: Option) -> bool { assert_one_yocto(); let account_id = env::predecessor_account_id(); - if let Some(account_deposit) = self.deposited_amounts.get(&account_id) { + if let Some(account_deposit) = self.accounts.get(&account_id) { // TODO: figure out force option logic. assert!( account_deposit.tokens.is_empty(), "ERR_STORAGE_UNREGISTER_TOKENS_NOT_EMPTY" ); - self.deposited_amounts.remove(&account_id); - Promise::new(account_id.clone()).transfer(account_deposit.amount); + self.accounts.remove(&account_id); + Promise::new(account_id.clone()).transfer(account_deposit.near_amount); true } else { false @@ -73,16 +73,16 @@ impl StorageManagement for Contract { fn storage_balance_bounds(&self) -> StorageBalanceBounds { StorageBalanceBounds { - min: AccountDeposit::min_storage_usage().into(), + min: (INIT_ACCOUNT_STORAGE as u128 * env::storage_byte_cost()).into(), max: None, } } fn storage_balance_of(&self, account_id: ValidAccountId) -> Option { - self.deposited_amounts + self.accounts .get(account_id.as_ref()) .map(|deposits| StorageBalance { - total: U128(deposits.amount), + total: U128(deposits.near_amount), available: U128(deposits.storage_available()), }) } diff --git a/ref-exchange/src/views.rs b/ref-exchange/src/views.rs index 8637c3f..ef037c7 100644 --- a/ref-exchange/src/views.rs +++ b/ref-exchange/src/views.rs @@ -89,7 +89,7 @@ impl Contract { /// Returns balances of the deposits for given user outside of any pools. /// Returns empty list if no tokens deposited. pub fn get_deposits(&self, account_id: ValidAccountId) -> HashMap { - self.deposited_amounts + self.accounts .get(account_id.as_ref()) .map(|d| { d.tokens @@ -102,7 +102,7 @@ impl Contract { /// Returns balance of the deposit for given user outside of any pools. pub fn get_deposit(&self, account_id: ValidAccountId, token_id: ValidAccountId) -> U128 { - self.internal_get_deposit(account_id.as_ref(), token_id.as_ref()) + self.get_deposit_balance(account_id.as_ref(), token_id.as_ref()) .into() } @@ -126,7 +126,7 @@ impl Contract { /// Get specific user whitelisted tokens. pub fn get_user_whitelisted_tokens(&self, account_id: &AccountId) -> Vec { - self.deposited_amounts + self.accounts .get(&account_id) .map(|d| d.tokens.keys().cloned().collect()) .unwrap_or_default() diff --git a/res/ref_exchange_local.wasm b/res/ref_exchange_local.wasm index c74ec346098e42a8e374ca99c03e06a26f66998d..3fb773d6caee1bfe5b3b043094513a0960844614 100755 GIT binary patch delta 40387 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 aVCh&Q70a4U%!#o-3g31o*B+&kUH<`Nl%XsD delta 40163 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 Date: Sat, 10 Apr 2021 16:39:03 +0200 Subject: [PATCH 2/6] wip --- ref-exchange/src/account_deposit.rs | 5 ++-- ref-exchange/src/lib.rs | 36 ++++++++++++++++++----------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index e51e816..bd944b6 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -73,7 +73,6 @@ impl AccountDeposit { } /// Returns amount of $NEAR necessary to cover storage used by account referenced to this structure. - #[inline] pub fn storage_usage(&self) -> Balance { (if self.storage_used < INIT_ACCOUNT_STORAGE { self.storage_used @@ -90,6 +89,7 @@ impl AccountDeposit { } /// Asserts there is sufficient amount of $NEAR to cover storage usage. + #[inline] pub fn assert_storage_usage(&self) { assert!( self.storage_usage() <= self.near_amount, @@ -100,8 +100,7 @@ impl AccountDeposit { /// Updates the account storage usage and sets. pub(crate) fn update_storage(&mut self, tx_start_storage: StorageUsage) { - let s = env::storage_usage(); - self.storage_used += s - tx_start_storage; + self.storage_used += env::storage_usage() - tx_start_storage; self.assert_storage_usage(); } diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index b5f3c98..e0067e7 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -6,7 +6,9 @@ use near_contract_standards::storage_management::{ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LookupMap, UnorderedSet, Vector}; use near_sdk::json_types::{ValidAccountId, U128}; -use near_sdk::{assert_one_yocto, env, log, near_bindgen, AccountId, PanicOnDefault, Promise}; +use near_sdk::{ + assert_one_yocto, env, log, near_bindgen, AccountId, PanicOnDefault, Promise, StorageUsage, +}; use crate::account_deposit::{AccountDeposit, INIT_ACCOUNT_STORAGE}; pub use crate::action::*; @@ -61,7 +63,10 @@ impl Contract { } } - /// Adds new "Simple Pool" with given tokens and given fee. + /// Adds new "Simple Pool" with given tokens and given fee. The effective pool fee is + /// `fee + exchange_fee + referralp_fee`. + /// This function doesn't set the initial price nor adds any liquidity to the pool. You must call the + /// `add_liquidity` for that. /// Deposited NEAR must be enough to cover the added storage. #[payable] pub fn add_simple_pool(&mut self, tokens: Vec, fee: u32) -> u64 { @@ -158,6 +163,15 @@ impl Contract { /// Internal methods implementation. impl Contract { + /// loads the accoutn from self.accounts, updates the storage used and asserts that there is enough NEAR + /// balance to cover storage cost. + fn update_acc_storage(&mut self, tx_start_storage: StorageUsage) { + let from = env::predecessor_account_id(); + let mut acc = self.get_account(&from); + acc.update_storage(tx_start_storage); + self.accounts.insert(&from, &acc); + } + /// Adds given pool to the list and returns it's id. /// If there is not enough attached balance to cover storage, fails. /// If too much attached - refunds it back. @@ -165,9 +179,7 @@ impl Contract { let start_storage = env::storage_usage(); let id = self.pools.len() as u64; self.pools.push(&pool); - - // TODO: update deposit handling - + self.update_acc_storage(start_storage); id } @@ -184,8 +196,8 @@ impl Contract { referral_id: &Option, ) -> u128 { let start_storage = env::storage_usage(); - let mut acc = self.accounts.get(&sender_id).unwrap_or_default(); - acc.sub(token_in, amount_in); + let mut acc = self.get_account(&sender_id); + acc.sub(token_in, amount_in); // NOTE: panics when there is not enough `token_in` deposit. let mut pool = self.pools.get(pool_id).expect("ERR_NO_POOL"); let amount_out = pool.swap( token_in, @@ -198,13 +210,11 @@ impl Contract { acc.add(token_out, amount_out); self.accounts.insert(&sender_id, &acc); self.pools.replace(pool_id, &pool); - // TODO: - // we need to insert 2 times: once in line 194 (prev) and second time here. - // This is because we won't trace properly new storage until we insert the record into the storage tree. - // Alternative would be to refactor the AccountDeposit and move ynear to another, top-level map. - // Better solution: compute the storage consumed by AccountDeposit on the fly. + // NOTE: this can cause changes in the deposits which increases an account storage (eg, if user doesn't + // have `token_out` in AccountDepoist, then a new record will be created). This is not a problem, + // because we compute the `AccountDepoist` storage consumption separaterly. acc.update_storage(start_storage); - + self.accounts.insert(&sender_id, &acc); amount_out } } From d4b3247c99f737050d730f713e8dbbb6a3f998d1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 10 Apr 2021 18:43:18 +0200 Subject: [PATCH 3/6] Add NEAR deposits --- ref-exchange/src/account_deposit.rs | 23 +++++++++++++++----- ref-exchange/src/lib.rs | 33 +++++++++++++++++++++++------ ref-exchange/src/simple_pool.rs | 4 ++-- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index bd944b6..0a660e0 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -126,6 +126,17 @@ impl AccountDeposit { #[near_bindgen] impl Contract { + /// Deposits attached NEAR into predecessor account deposits. The deposited near will be used + /// for trades and for storage. Predecessor account must be registered. Panics otherwise. + /// NOTE: this is a simplified and more direct version of `storage_deposit` function. + #[payable] + pub fn deposit_near(&mut self) { + let sender_id = env::predecessor_account_id(); + let mut acc = self.get_account(&sender_id); + acc.near_amount += env::attached_deposit(); + self.accounts.insert(&sender_id, &acc); + } + /// Registers given token in the user's account deposit. /// Fails if not enough balance on this account to cover storage. pub fn register_tokens(&mut self, token_ids: Vec) { @@ -244,15 +255,17 @@ impl Contract { self.accounts.get(from).expect(ERR10_ACC_NOT_REGISTERED) } - /// Returns current balance of given token for given user. If there is nothing recorded, returns 0. + /// Returns current balance of given token for given user. If token_id == "" then returns NEAR (native) + /// balance. If there is nothing recorded, returns 0. pub(crate) fn get_deposit_balance( &self, sender_id: &AccountId, token_id: &AccountId, ) -> Balance { - self.accounts - .get(sender_id) - .and_then(|d| d.tokens.get(token_id).cloned()) - .unwrap_or_default() + let acc = self.get_account(sender_id); + if token_id == "" { + return acc.near_amount; + } + *acc.tokens.get(token_id).unwrap_or(&0) } } diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index e0067e7..2f9c9e1 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -107,7 +107,7 @@ impl Contract { U128(prev_amount.expect("ERR_AT_LEAST_ONE_SWAP")) } - /// Add liquidity from already deposited amounts to given pool. + /// Add liquidity from already deposited amounts to the given pool. #[payable] pub fn add_liquidity( &mut self, @@ -116,6 +116,7 @@ impl Contract { min_amounts: Option>, ) { assert_one_yocto(); + let start_storage = env::storage_usage(); let sender_id = env::predecessor_account_id(); let mut amounts: Vec = amounts.into_iter().map(|amount| amount.into()).collect(); let mut pool = self.pools.get(pool_id).expect("ERR_NO_POOL"); @@ -129,18 +130,21 @@ impl Contract { } let mut acc = self.get_account(&sender_id); let tokens = pool.tokens(); - // Subtract updated amounts from deposits. This will fail if there is not enough funds for any of the tokens. + // Subtract updated amounts from deposits. Fails if there is not enough funds for any of the tokens. for i in 0..tokens.len() { acc.sub(&tokens[i], amounts[i]); } - self.accounts.insert(&sender_id, &acc); self.pools.replace(pool_id, &pool); + // Can create a new shares record in a pool + acc.update_storage(start_storage); + self.accounts.insert(&sender_id, &acc); } - /// Remove liquidity from the pool into general pool of liquidity. + /// Remove liquidity from the pool and transfer it into account deposit. #[payable] pub fn remove_liquidity(&mut self, pool_id: u64, shares: U128, min_amounts: Vec) { assert_one_yocto(); + let start_storage = env::storage_usage(); let sender_id = env::predecessor_account_id(); let mut pool = self.pools.get(pool_id).expect("ERR_NO_POOL"); let amounts = pool.remove_liquidity( @@ -157,6 +161,8 @@ impl Contract { for i in 0..tokens.len() { acc.add(&tokens[i], amounts[i]); } + // Can remove shares record in a pool + acc.update_storage(start_storage); self.accounts.insert(&sender_id, &acc); } } @@ -210,9 +216,10 @@ impl Contract { acc.add(token_out, amount_out); self.accounts.insert(&sender_id, &acc); self.pools.replace(pool_id, &pool); - // NOTE: this can cause changes in the deposits which increases an account storage (eg, if user doesn't - // have `token_out` in AccountDepoist, then a new record will be created). This is not a problem, - // because we compute the `AccountDepoist` storage consumption separaterly. + // NOTE: this can cause changes in the deposits which increases an account storage (eg, + // if user doesn't have `token_out` in AccountDepoist, then a new record will be created). + // This is not a problem, because we compute the `AccountDepoist` storage consumption + // separaterly, hence we must do this update. acc.update_storage(start_storage); self.accounts.insert(&sender_id, &acc); amount_out @@ -381,6 +388,18 @@ mod tests { None, ); assert_eq!(contract.get_deposit(accounts(3), accounts(1)).0, 0); + + // must work with NEAR deposits + testing_env!(context + .predecessor_account_id(accounts(3)) + .attached_deposit(100) + .build()); + assert_eq!( + contract + .get_deposit(accounts(3), "".to_string().try_into().unwrap()) + .0, + 100 + ); } /// Test liquidity management. diff --git a/ref-exchange/src/simple_pool.rs b/ref-exchange/src/simple_pool.rs index bda6bb2..9217268 100644 --- a/ref-exchange/src/simple_pool.rs +++ b/ref-exchange/src/simple_pool.rs @@ -316,7 +316,7 @@ mod tests { accounts(2).as_ref(), 1, accounts(3).as_ref(), - Some(accounts(4).as_ref().clone()), + &Some(accounts(4).as_ref().clone()), ); assert_eq!( pool.share_balances(accounts(0).as_ref()), @@ -348,7 +348,7 @@ mod tests { accounts(2).as_ref(), 1, accounts(3).as_ref(), - Some(accounts(4).as_ref().clone()), + &Some(accounts(4).as_ref().clone()), ); assert_eq!( pool.share_balances(accounts(0).as_ref()), From 2b51e5c1c166d6a610d804e8a5202765e82be0f4 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 10 Apr 2021 18:58:31 +0200 Subject: [PATCH 4/6] update the storage_used function --- ref-exchange/src/account_deposit.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index 0a660e0..0cb6f6d 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -12,12 +12,16 @@ use near_sdk::{ use crate::utils::{ext_fungible_token, ext_self, GAS_FOR_FT_TRANSFER}; use crate::*; +const U128_STORAGE: StorageUsage = 16; /// bytes length of u64 values const U64_STORAGE: StorageUsage = 8; /// bytes length of u32 values. Used in length operations const U32_STORAGE: StorageUsage = 4; +/// max length of account id * 8 (1 byte) +const ACC_ID_STORAGE: StorageUsage = 64 * 8; // 64 = max account name length -pub const INIT_ACCOUNT_STORAGE: StorageUsage = 64 + 2 * U64_STORAGE + U32_STORAGE; +pub const INIT_ACCOUNT_STORAGE: StorageUsage = + ACC_ID_STORAGE + U128_STORAGE + U32_STORAGE + 2 * U64_STORAGE; // NEAR native token. This is not a valid token ID. HACK: NEAR is a native token, we use the // empty string we use it to reference not existing near account. @@ -74,15 +78,13 @@ impl AccountDeposit { /// Returns amount of $NEAR necessary to cover storage used by account referenced to this structure. pub fn storage_usage(&self) -> Balance { - (if self.storage_used < INIT_ACCOUNT_STORAGE { - self.storage_used - } else { - INIT_ACCOUNT_STORAGE - }) as Balance - * env::storage_byte_cost() + let s = self.storage_used + + INIT_ACCOUNT_STORAGE // empty account storage + + (ACC_ID_STORAGE + U64_STORAGE) * self.tokens.len() as u64; // self.tokens storage + return s as Balance * env::storage_byte_cost(); } - /// Returns how much NEAR is available for storage. + /// Returns how much NEAR is available for storage and swaps. #[inline] pub(crate) fn storage_available(&self) -> Balance { self.near_amount - self.storage_usage() @@ -98,14 +100,15 @@ impl AccountDeposit { ); } - /// Updates the account storage usage and sets. + /// Updates the account storage usage. + /// Panics if there is not enought $NEAR to cover storage usage. pub(crate) fn update_storage(&mut self, tx_start_storage: StorageUsage) { self.storage_used += env::storage_usage() - tx_start_storage; self.assert_storage_usage(); } /// Registers given `token_id` and set balance to 0. - /// Fails if not enough NEAR is in deposit to cover new storage usage. + /// Panics if there is not enought $NEAR to cover storage usage. pub(crate) fn register(&mut self, token_ids: &Vec) { for token_id in token_ids { let t = token_id.as_ref(); From ec470c8ee959e07747f74dd6849a156fe780116a Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 12 Apr 2021 16:07:25 +0200 Subject: [PATCH 5/6] add migration --- ref-exchange/src/account_deposit.rs | 90 ++++++++++++++++++++++++----- ref-exchange/src/legacy.rs | 4 +- ref-exchange/src/lib.rs | 12 ++-- ref-exchange/src/owner.rs | 2 +- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index 0cb6f6d..088a44f 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -20,17 +20,55 @@ const U32_STORAGE: StorageUsage = 4; /// max length of account id * 8 (1 byte) const ACC_ID_STORAGE: StorageUsage = 64 * 8; // 64 = max account name length + +// ACC_ID: the Contract accounts map +// + U128_STORAGE: near_amount storage +// + U32_STORAGE: tokens HashMap length +// + U64_STORAGE: storage_used +// + 2: version pub const INIT_ACCOUNT_STORAGE: StorageUsage = - ACC_ID_STORAGE + U128_STORAGE + U32_STORAGE + 2 * U64_STORAGE; + ACC_ID_STORAGE + U128_STORAGE + U32_STORAGE + 2 * U64_STORAGE + 2; // NEAR native token. This is not a valid token ID. HACK: NEAR is a native token, we use the // empty string we use it to reference not existing near account. // pub const NEAR: AccountId = "".to_string(); +#[derive(BorshDeserialize, BorshSerialize)] +pub enum AccountDeposit { + V2(AccountDepositV2), +} + +impl From for AccountDepositV2 { + fn from(account: AccountDeposit) -> Self { + match account { + AccountDeposit::V2(a) => { + if a.storage_used > 0 { + return a; + } + // migrate from V1 + a.storage_used = U64_STORAGE; + a + } + } + } +} + +/// Account deposits information and storage cost. +/// Legacy version +#[derive(BorshSerialize, BorshDeserialize, Default)] +#[cfg_attr(feature = "test", derive(Clone))] +pub struct AccountDepositV1 { + /// NEAR sent to the exchange. + /// Used for storage and trading. + pub near_amount: Balance, + /// Amounts of various tokens in this account. + pub tokens: HashMap, +} + /// Account deposits information and storage cost. #[derive(BorshSerialize, BorshDeserialize, Default)] #[cfg_attr(feature = "test", derive(Clone))] -pub struct AccountDeposit { +pub struct AccountDepositV2 { /// NEAR sent to the exchange. /// Used for storage and trading. pub near_amount: Balance, @@ -39,13 +77,19 @@ pub struct AccountDeposit { pub storage_used: StorageUsage, } -impl AccountDeposit { +impl From for AccountDeposit { + fn from(a: AccountDepositV2) -> Self { + AccountDeposit::V2(a) + } +} + +impl AccountDepositV2 { pub fn new(account_id: &AccountId, near_amount: Balance) -> Self { Self { near_amount, tokens: HashMap::default(), // Here we manually compute the initial storage size of account deposit. - storage_used: account_id.len() as StorageUsage + U64_STORAGE + U32_STORAGE, + storage_used: U64_STORAGE, } } @@ -137,16 +181,16 @@ impl Contract { let sender_id = env::predecessor_account_id(); let mut acc = self.get_account(&sender_id); acc.near_amount += env::attached_deposit(); - self.accounts.insert(&sender_id, &acc); + self.accounts.insert(&sender_id, &acc.into()); } /// Registers given token in the user's account deposit. /// Fails if not enough balance on this account to cover storage. pub fn register_tokens(&mut self, token_ids: Vec) { let sender_id = env::predecessor_account_id(); - let mut deposits = self.get_account(&sender_id); - deposits.register(&token_ids); - self.accounts.insert(&sender_id, &deposits); + let mut acc = self.get_account(&sender_id); + acc.register(&token_ids); + self.accounts.insert(&sender_id, &acc.into()); } /// Unregister given token from user's account deposit. @@ -157,7 +201,7 @@ impl Contract { for token_id in token_ids { deposits.unregister(token_id.as_ref()); } - self.accounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits.into()); } /// Withdraws given token from the deposits of given user. @@ -175,7 +219,7 @@ impl Contract { if unregister == Some(true) { deposits.unregister(&token_id); } - self.accounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits.into()); ext_fungible_token::ft_transfer( sender_id.clone().try_into().unwrap(), amount.into(), @@ -214,7 +258,7 @@ impl Contract { // This reverts the changes from withdraw function. let mut deposits = self.get_account(&sender_id); deposits.add(&token_id, amount.0); - self.accounts.insert(&sender_id, &deposits); + self.accounts.insert(&sender_id, &deposits.into()); } }; } @@ -229,7 +273,7 @@ impl Contract { account_deposit.near_amount += amount; account_deposit } else { - AccountDeposit::new(account_id, amount) + AccountDepositV2::new(account_id, amount) }; self.accounts.insert(&account_id, &acc); } @@ -249,13 +293,29 @@ impl Contract { ERR12_TOKEN_NOT_WHITELISTED ); acc.add(token_id, amount); - self.accounts.insert(sender_id, &acc); + self.accounts.insert(sender_id, &acc.into()); } // Returns `from` AccountDeposit. #[inline] - pub(crate) fn get_account(&self, from: &AccountId) -> AccountDeposit { - self.accounts.get(from).expect(ERR10_ACC_NOT_REGISTERED) + pub(crate) fn get_account(&self, from: &AccountId) -> AccountDepositV2 { + self.accounts + .get(from) + .expect(ERR10_ACC_NOT_REGISTERED) + .into() + } + + pub(crate) fn get_account_option(&self, from: &AccountId) -> Option { + // let key = ("d".to_owned() + from).into_bytes(); + // let data = env::storage_read(&key); + // if data == None { + // return None; + // } + // let Some(data) = data; + // AccountDepositV1::Dese + // borsh::de:: + + self.accounts.get(from).and_then(|a| a.into()) } /// Returns current balance of given token for given user. If token_id == "" then returns NEAR (native) diff --git a/ref-exchange/src/legacy.rs b/ref-exchange/src/legacy.rs index 33dc7c6..c51871d 100644 --- a/ref-exchange/src/legacy.rs +++ b/ref-exchange/src/legacy.rs @@ -4,7 +4,7 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LookupMap, Vector}; use near_sdk::AccountId; -use crate::account_deposit::AccountDeposit; +use crate::account_deposit::AccountDepositV2; use crate::pool::Pool; /// Version before whitelisted tokens collection. @@ -19,5 +19,5 @@ pub struct ContractV1 { /// List of all the pools. pub pools: Vector, /// Balances of deposited tokens for each account. - pub deposited_amounts: LookupMap, + pub deposited_amounts: LookupMap, } diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index 2f9c9e1..6d36ddf 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -10,7 +10,7 @@ use near_sdk::{ assert_one_yocto, env, log, near_bindgen, AccountId, PanicOnDefault, Promise, StorageUsage, }; -use crate::account_deposit::{AccountDeposit, INIT_ACCOUNT_STORAGE}; +use crate::account_deposit::{AccountDeposit, AccountDepositV1, INIT_ACCOUNT_STORAGE}; pub use crate::action::*; use crate::errors::*; use crate::pool::Pool; @@ -137,7 +137,7 @@ impl Contract { self.pools.replace(pool_id, &pool); // Can create a new shares record in a pool acc.update_storage(start_storage); - self.accounts.insert(&sender_id, &acc); + self.accounts.insert(&sender_id, &acc.into()); } /// Remove liquidity from the pool and transfer it into account deposit. @@ -163,7 +163,7 @@ impl Contract { } // Can remove shares record in a pool acc.update_storage(start_storage); - self.accounts.insert(&sender_id, &acc); + self.accounts.insert(&sender_id, &acc.into()); } } @@ -175,7 +175,7 @@ impl Contract { let from = env::predecessor_account_id(); let mut acc = self.get_account(&from); acc.update_storage(tx_start_storage); - self.accounts.insert(&from, &acc); + self.accounts.insert(&from, &acc.into()); } /// Adds given pool to the list and returns it's id. @@ -214,14 +214,14 @@ impl Contract { referral_id, ); acc.add(token_out, amount_out); - self.accounts.insert(&sender_id, &acc); + self.accounts.insert(&sender_id, &acc.into()); self.pools.replace(pool_id, &pool); // NOTE: this can cause changes in the deposits which increases an account storage (eg, // if user doesn't have `token_out` in AccountDepoist, then a new record will be created). // This is not a problem, because we compute the `AccountDepoist` storage consumption // separaterly, hence we must do this update. acc.update_storage(start_storage); - self.accounts.insert(&sender_id, &acc); + self.accounts.insert(&sender_id, &acc.into()); amount_out } } diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index b982f2f..6d174fb 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -6,7 +6,7 @@ use crate::legacy::ContractV1; use crate::utils::{GAS_FOR_DEPLOY_CALL, GAS_FOR_UPGRADE_CALL}; #[near_bindgen] -impl Contract { +impl ContractV1 { /// Change owner. Only can be called by owner. pub fn set_owner(&mut self, owner_id: ValidAccountId) { self.assert_owner(); From 7e8b9ae18be8353e3cea24f5452dd0fa3608ddb8 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Apr 2021 15:54:36 +0200 Subject: [PATCH 6/6] review updates --- ref-exchange/src/account_deposit.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ref-exchange/src/account_deposit.rs b/ref-exchange/src/account_deposit.rs index 088a44f..1fe9e5a 100644 --- a/ref-exchange/src/account_deposit.rs +++ b/ref-exchange/src/account_deposit.rs @@ -17,17 +17,15 @@ const U128_STORAGE: StorageUsage = 16; const U64_STORAGE: StorageUsage = 8; /// bytes length of u32 values. Used in length operations const U32_STORAGE: StorageUsage = 4; -/// max length of account id * 8 (1 byte) -const ACC_ID_STORAGE: StorageUsage = 64 * 8; -// 64 = max account name length +/// max length of account id is 64 bytes. We charge per byte. +const ACC_ID_STORAGE: StorageUsage = 64; // ACC_ID: the Contract accounts map // + U128_STORAGE: near_amount storage // + U32_STORAGE: tokens HashMap length // + U64_STORAGE: storage_used -// + 2: version pub const INIT_ACCOUNT_STORAGE: StorageUsage = - ACC_ID_STORAGE + U128_STORAGE + U32_STORAGE + 2 * U64_STORAGE + 2; + ACC_ID_STORAGE + U128_STORAGE + U32_STORAGE + 2 * U64_STORAGE; // NEAR native token. This is not a valid token ID. HACK: NEAR is a native token, we use the // empty string we use it to reference not existing near account.