diff --git a/ref-exchange/Cargo.toml b/ref-exchange/Cargo.toml index cd9b8c1..57e8023 100644 --- a/ref-exchange/Cargo.toml +++ b/ref-exchange/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ref-exchange" -version = "0.2.1" +version = "0.3.0" authors = ["Illia Polosukhin "] edition = "2018" publish = false diff --git a/ref-exchange/src/legacy.rs b/ref-exchange/src/legacy.rs index 8430173..b226642 100644 --- a/ref-exchange/src/legacy.rs +++ b/ref-exchange/src/legacy.rs @@ -1 +1,28 @@ //! This modules captures all the code needed to migrate from previous version. + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{LookupMap, UnorderedSet, Vector}; +use near_sdk::{near_bindgen, AccountId,}; + +use crate::account_deposit::Account; +use crate::pool::Pool; + + + + +#[near_bindgen] +#[derive(BorshSerialize, BorshDeserialize)] +pub struct OldContract { + /// Account of the owner. + pub owner_id: AccountId, + /// Exchange fee, that goes to exchange itself (managed by governance). + exchange_fee: u32, + /// Referral fee, that goes to referrer in the call. + referral_fee: u32, + /// List of all the pools. + pub pools: Vector, + /// Accounts registered, keeping track all the amounts deposited, storage and more. + pub accounts: LookupMap, + /// Set of whitelisted tokens by "owner". + pub whitelisted_tokens: UnorderedSet, +} diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index 367a9db..fb02b2f 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -6,6 +6,7 @@ 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::serde::{Deserialize, Serialize}; use near_sdk::{ assert_one_yocto, env, log, near_bindgen, AccountId, Balance, PanicOnDefault, Promise, PromiseResult, StorageUsage, @@ -35,15 +36,22 @@ mod views; near_sdk::setup_alloc!(); +#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct InternalFeesRatio { + /// Portion (bps) of the fee going to exchange in total fee. + pub exchange_fee: u32, + /// Portion (bps) of the fee going to referral in total fee. + pub referral_fee: u32, +} + #[near_bindgen] #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] pub struct Contract { /// Account of the owner. owner_id: AccountId, - /// Exchange fee, that goes to exchange itself (managed by governance). - exchange_fee: u32, - /// Referral fee, that goes to referrer in the call. - referral_fee: u32, + /// Exchange fee and referral_fee (managed by governance). + fee_policy: InternalFeesRatio, /// List of all the pools. pools: Vector, /// Accounts registered, keeping track all the amounts deposited, storage and more. @@ -55,11 +63,10 @@ pub struct Contract { #[near_bindgen] impl Contract { #[init] - pub fn new(owner_id: ValidAccountId, exchange_fee: u32, referral_fee: u32) -> Self { + pub fn new(owner_id: ValidAccountId, fee_policy: InternalFeesRatio) -> Self { Self { owner_id: owner_id.as_ref().clone(), - exchange_fee, - referral_fee, + fee_policy, pools: Vector::new(b"p".to_vec()), accounts: LookupMap::new(b"d".to_vec()), whitelisted_tokens: UnorderedSet::new(b"w".to_vec()), @@ -74,9 +81,7 @@ impl Contract { self.internal_add_pool(Pool::SimplePool(SimplePool::new( self.pools.len() as u32, tokens, - fee + self.exchange_fee + self.referral_fee, - self.exchange_fee, - self.referral_fee, + fee, ))) } @@ -284,6 +289,7 @@ impl Contract { min_amount_out, &self.owner_id, referral_id, + &self.fee_policy, ); self.pools.replace(pool_id, &pool); amount_out @@ -305,7 +311,13 @@ mod tests { fn setup_contract() -> (VMContextBuilder, Contract) { let mut context = VMContextBuilder::new(); testing_env!(context.predecessor_account_id(accounts(0)).build()); - let contract = Contract::new(accounts(0), 4, 1); + let contract = Contract::new( + accounts(0), + InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + } + ); (context, contract) } @@ -357,7 +369,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .attached_deposit(env::storage_byte_cost() * 300) .build()); - let pool_id = contract.add_simple_pool(tokens, 25); + let pool_id = contract.add_simple_pool(tokens, 30); testing_env!(context .predecessor_account_id(account_id.clone()) .attached_deposit(to_yocto("0.03")) @@ -476,7 +488,7 @@ mod tests { // Exchange fees left in the pool as liquidity + 1m from transfer. assert_eq!( contract.get_pool_total_shares(0).0, - 33337501041992301475 + 1_000_000 + 50006251562988452212 + 1_000_000 ); contract.withdraw( @@ -718,7 +730,7 @@ mod tests { ], None, ); - // Roundtrip returns almost everything except 0.3% fee. + // Roundtrip returns almost everything except 0.25% fee. assert_eq!(contract.get_deposit(acc, accounts(1)).0, 1_000_000 - 7); } } diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index 2e11434..7077aad 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -1,6 +1,9 @@ //! Implement all the relevant logic for owner of this contract. use crate::*; +use crate::utils::FEE_DIVISOR; +use crate::legacy::OldContract; + #[near_bindgen] impl Contract { @@ -32,12 +35,31 @@ impl Contract { } } - /// Migration function from v2 to v2. + /// Set fee policy. + pub fn set_fee_policy(&mut self, fee_policy: InternalFeesRatio) { + self.assert_owner(); + assert!( + fee_policy.exchange_fee + fee_policy.referral_fee <= FEE_DIVISOR, + "ERR_FEE_TOO_LARGE" + ); + self.fee_policy = fee_policy; + } + + /// Migration function from v2 to v3. /// For next version upgrades, change this function. #[init(ignore_state)] pub fn migrate() -> Self { - let contract: Contract = env::state_read().expect("ERR_NOT_INITIALIZED"); - contract + let old_contract: OldContract = env::state_read().expect("ERR_NOT_INITIALIZED"); + Contract { + owner_id: old_contract.owner_id, + fee_policy: InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + }, + pools: old_contract.pools, + accounts: old_contract.accounts, + whitelisted_tokens: old_contract.whitelisted_tokens, + } } pub(crate) fn assert_owner(&self) { diff --git a/ref-exchange/src/pool.rs b/ref-exchange/src/pool.rs index 62f650a..1640549 100644 --- a/ref-exchange/src/pool.rs +++ b/ref-exchange/src/pool.rs @@ -3,6 +3,7 @@ use near_sdk::{AccountId, Balance}; use crate::simple_pool::SimplePool; use crate::utils::SwapVolume; +use crate::InternalFeesRatio; /// Generic Pool, providing wrapper around different implementations of swap pools. /// Allows to add new types of pools just by adding extra item in the enum without needing to migrate the storage. @@ -81,6 +82,7 @@ impl Pool { min_amount_out: Balance, exchange_id: &AccountId, referral_id: &Option, + fee_policy: &InternalFeesRatio, ) -> Balance { match self { Pool::SimplePool(pool) => pool.swap( @@ -90,6 +92,7 @@ impl Pool { min_amount_out, exchange_id, referral_id, + fee_policy, ), } } diff --git a/ref-exchange/src/simple_pool.rs b/ref-exchange/src/simple_pool.rs index fc1d874..d340351 100644 --- a/ref-exchange/src/simple_pool.rs +++ b/ref-exchange/src/simple_pool.rs @@ -4,6 +4,7 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::LookupMap; use near_sdk::json_types::ValidAccountId; use near_sdk::{env, AccountId, Balance}; +use crate::InternalFeesRatio; use crate::errors::{ ERR13_LP_NOT_REGISTERED, ERR14_LP_ALREADY_REGISTERED, ERR31_ZERO_AMOUNT, ERR32_ZERO_SHARES, @@ -27,9 +28,9 @@ pub struct SimplePool { pub volumes: Vec, /// Fee charged for swap (gets divided by FEE_DIVISOR). pub total_fee: u32, - /// Portion of the fee going to exchange. + /// obsolete, not used in any logic, reserved for storage integrity pub exchange_fee: u32, - /// Portion of the fee going to referral. + /// obsolete, not used in any logic, reserved for storage integrity pub referral_fee: u32, /// Shares of the pool by liquidity providers. pub shares: LookupMap, @@ -42,11 +43,9 @@ impl SimplePool { id: u32, token_account_ids: Vec, total_fee: u32, - exchange_fee: u32, - referral_fee: u32, ) -> Self { assert!( - total_fee < FEE_DIVISOR && (exchange_fee + referral_fee) <= total_fee, + total_fee < FEE_DIVISOR, "ERR_FEE_TOO_LARGE" ); assert_ne!(token_account_ids.len(), 1, "ERR_NOT_ENOUGH_TOKENS"); @@ -59,8 +58,8 @@ impl SimplePool { amounts: vec![0u128; token_account_ids.len()], volumes: vec![SwapVolume::default(); token_account_ids.len()], total_fee, - exchange_fee, - referral_fee, + exchange_fee: 0, + referral_fee: 0, shares: LookupMap::new(format!("s{}", id).into_bytes()), shares_total_supply: 0, } @@ -267,6 +266,7 @@ impl SimplePool { min_amount_out: Balance, exchange_id: &AccountId, referral_id: &Option, + fee_policy: &InternalFeesRatio, ) -> Balance { let in_idx = self.token_index(token_in); let out_idx = self.token_index(token_out); @@ -295,18 +295,18 @@ impl SimplePool { let numerator = (new_invariant - prev_invariant) * U256::from(self.shares_total_supply); // Allocate exchange fee as fraction of total fee by issuing LP shares proportionally. - if self.exchange_fee > 0 && numerator > U256::zero() { - let denominator = new_invariant * self.total_fee / self.exchange_fee; + if fee_policy.exchange_fee > 0 && numerator > U256::zero() { + let denominator = new_invariant * FEE_DIVISOR / fee_policy.exchange_fee; self.mint_shares(&exchange_id, (numerator / denominator).as_u128()); } // If there is referral provided and the account already registered LP, allocate it % of LP rewards. if let Some(referral_id) = referral_id { - if self.referral_fee > 0 + if fee_policy.referral_fee > 0 && numerator > U256::zero() && self.shares.contains_key(referral_id) { - let denominator = new_invariant * self.total_fee / self.referral_fee; + let denominator = new_invariant * FEE_DIVISOR / fee_policy.referral_fee; self.mint_shares(&referral_id, (numerator / denominator).as_u128()); } } @@ -334,7 +334,7 @@ mod tests { let mut context = VMContextBuilder::new(); context.predecessor_account_id(accounts(0)); testing_env!(context.build()); - let mut pool = SimplePool::new(0, vec![accounts(1), accounts(2)], 30, 0, 0); + let mut pool = SimplePool::new(0, vec![accounts(1), accounts(2)], 20); let mut amounts = vec![to_yocto("5"), to_yocto("10")]; let num_shares = pool.add_liquidity(accounts(0).as_ref(), &mut amounts); assert_eq!(amounts, vec![to_yocto("5"), to_yocto("10")]); @@ -349,6 +349,10 @@ mod tests { 1, accounts(3).as_ref(), &None, + &InternalFeesRatio { + exchange_fee: 0, + referral_fee: 0, + } ); assert_eq!( pool.share_balance_of(accounts(0).as_ref()), @@ -380,7 +384,7 @@ mod tests { let mut context = VMContextBuilder::new(); context.predecessor_account_id(accounts(0)); testing_env!(context.build()); - let mut pool = SimplePool::new(0, vec![accounts(1), accounts(2)], 100, 100, 0); + let mut pool = SimplePool::new(0, vec![accounts(1), accounts(2)], 100); let mut amounts = vec![to_yocto("5"), to_yocto("10")]; let num_shares = pool.add_liquidity(accounts(0).as_ref(), &mut amounts); assert_eq!(amounts, vec![to_yocto("5"), to_yocto("10")]); @@ -395,6 +399,10 @@ mod tests { 1, accounts(3).as_ref(), &None, + &InternalFeesRatio { + exchange_fee: 10000, + referral_fee: 0, + } ); assert_eq!( pool.share_balance_of(accounts(0).as_ref()), diff --git a/ref-exchange/src/views.rs b/ref-exchange/src/views.rs index 3d62cc1..63726b2 100644 --- a/ref-exchange/src/views.rs +++ b/ref-exchange/src/views.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::json_types::{ValidAccountId, U128, U64}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{near_bindgen, AccountId}; @@ -39,8 +39,32 @@ impl From for PoolInfo { } } + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Metadata { + pub version: String, + pub owner_id: AccountId, + pub pool_count: U64, + pub whitelist_count: U64, + pub fee_policy: InternalFeesRatio, +} + + #[near_bindgen] impl Contract { + + /// meta_data of this contract + pub fn get_metadata(&self) -> Metadata { + Metadata { + owner_id: self.owner_id.clone(), + version: env!("CARGO_PKG_VERSION").to_string(), + pool_count: self.pools.len().into(), + whitelist_count: self.whitelisted_tokens.len().into(), + fee_policy: self.fee_policy.clone(), + } + } + /// Returns semver of this contract. pub fn version(&self) -> String { env!("CARGO_PKG_VERSION").to_string() diff --git a/ref-exchange/tests/test_migrate.rs b/ref-exchange/tests/test_migrate.rs index 69c0f2c..2905a67 100644 --- a/ref-exchange/tests/test_migrate.rs +++ b/ref-exchange/tests/test_migrate.rs @@ -4,6 +4,7 @@ use near_sdk::json_types::ValidAccountId; use near_sdk_sim::{deploy, init_simulator, to_yocto}; use ref_exchange::ContractContract as Exchange; +use ref_exchange::InternalFeesRatio; near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { PREV_EXCHANGE_WASM_BYTES => "../res/ref_exchange_local.wasm", @@ -19,7 +20,11 @@ fn test_upgrade() { contract_id: "swap".to_string(), bytes: &PREV_EXCHANGE_WASM_BYTES, signer_account: root, - init_method: new(ValidAccountId::try_from(root.account_id.clone()).unwrap(), 4, 1) + init_method: new(ValidAccountId::try_from(root.account_id.clone()).unwrap(), + InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + }) ); // Failed upgrade with no permissions. let result = test_user diff --git a/ref-exchange/tests/test_swap.rs b/ref-exchange/tests/test_swap.rs index d553685..c1fc4b2 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::{ContractContract as Exchange, PoolInfo, SwapAction, InternalFeesRatio}; use test_token::ContractContract as TestToken; near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { @@ -105,7 +105,10 @@ fn setup_pool_with_liquidity() -> ( contract_id: swap(), bytes: &EXCHANGE_WASM_BYTES, signer_account: root, - init_method: new(to_va("owner".to_string()), 4, 1) + init_method: new(to_va("owner".to_string()), InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + }) ); let token1 = test_token(&root, dai(), vec![swap()]); let token2 = test_token(&root, eth(), vec![swap()]); @@ -115,7 +118,7 @@ fn setup_pool_with_liquidity() -> ( ); call!( root, - pool.add_simple_pool(vec![to_va(dai()), to_va(eth())], 25), + pool.add_simple_pool(vec![to_va(dai()), to_va(eth())], 30), deposit = to_yocto("1") ) .assert_success(); @@ -240,7 +243,10 @@ fn test_withdraw_failure() { contract_id: swap(), bytes: &EXCHANGE_WASM_BYTES, signer_account: root, - init_method: new(to_va("owner".to_string()), 4, 1) + init_method: new(to_va("owner".to_string()), InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + }) ); // Deploy DAI and wETH fungible tokens let dai_contract = test_token(&root, dai(), vec![swap()]); diff --git a/ref-farming/tests/common/init.rs b/ref-farming/tests/common/init.rs index 1b8746a..c4131a1 100644 --- a/ref-farming/tests/common/init.rs +++ b/ref-farming/tests/common/init.rs @@ -5,7 +5,7 @@ use near_sdk::{AccountId}; use near_sdk_sim::{call, deploy, to_yocto, ContractAccount, UserAccount}; // use near_sdk_sim::transaction::ExecutionStatus; -use ref_exchange::{ContractContract as TestRef}; +use ref_exchange::{ContractContract as TestRef, InternalFeesRatio}; use test_token::ContractContract as TestToken; use ref_farming::{ContractContract as Farming}; @@ -34,7 +34,13 @@ pub fn deploy_pool(root: &UserAccount, contract_id: AccountId, owner_id: Account contract_id: contract_id, bytes: &EXCHANGE_WASM_BYTES, signer_account: root, - init_method: new(to_va(owner_id), 4, 1) + init_method: new( + to_va(owner_id), + InternalFeesRatio { + exchange_fee: 2000, + referral_fee: 500, + } + ) ); pool } diff --git a/res/ref_exchange_release.wasm b/res/ref_exchange_release.wasm index 7b0456e..b913f9e 100755 Binary files a/res/ref_exchange_release.wasm and b/res/ref_exchange_release.wasm differ