From 2d85081508da12dd3b880b003f7c4a99a6ac80af Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Thu, 1 Feb 2024 16:50:44 +0700 Subject: [PATCH 01/13] feat: unbonding time --- contracts/oraiswap_staking/src/contract.rs | 33 ++++- contracts/oraiswap_staking/src/rewards.rs | 5 +- contracts/oraiswap_staking/src/staking.rs | 150 ++++++++++++++++++--- contracts/oraiswap_staking/src/state.rs | 101 +++++++++++++- packages/oraiswap/src/staking.rs | 14 +- 5 files changed, 270 insertions(+), 33 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index ed121177..7f76dce8 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -10,12 +10,12 @@ use crate::rewards::{ deposit_reward, process_reward_assets, query_all_reward_infos, query_reward_info, withdraw_reward, withdraw_reward_others, }; -use crate::staking::{auto_stake, auto_stake_hook, bond, unbond}; +use crate::staking::{auto_stake, auto_stake_hook, bond, unbond, unbond_lock}; use crate::state::{ read_all_pool_infos, read_config, read_finish_migrate_store_status, read_pool_info, read_rewards_per_sec, remove_pool_info, stakers_read, store_config, - store_finish_migrate_store_status, store_pool_info, store_rewards_per_sec, Config, - MigrationParams, PoolInfo, + store_finish_migrate_store_status, store_pool_info, store_rewards_per_sec, + store_unbonding_period, Config, MigrationParams, PoolInfo, }; use cosmwasm_std::{ @@ -70,7 +70,10 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S assets, } => update_rewards_per_sec(deps, info, staking_token, assets), ExecuteMsg::DepositReward { rewards } => deposit_reward(deps, info, rewards), - ExecuteMsg::RegisterAsset { staking_token } => register_asset(deps, info, staking_token), + ExecuteMsg::RegisterAsset { + staking_token, + unbonding_period, + } => register_asset(deps, info, staking_token, unbonding_period), ExecuteMsg::DeprecateStakingToken { staking_token, new_staking_token, @@ -100,6 +103,10 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S staker_addr, prev_staking_token_amount, ), + ExecuteMsg::UnbondLock { + staking_token, + lock_id, + } => unbond_lock(deps, env, info.sender, staking_token, lock_id), } } @@ -217,7 +224,12 @@ fn update_rewards_per_sec( Ok(Response::new().add_attribute("action", "update_rewards_per_sec")) } -fn register_asset(deps: DepsMut, info: MessageInfo, staking_token: Addr) -> StdResult { +fn register_asset( + deps: DepsMut, + info: MessageInfo, + staking_token: Addr, + unbonding_period: Option, +) -> StdResult { validate_migrate_store_status(deps.storage)?; let config: Config = read_config(deps.storage)?; @@ -243,9 +255,20 @@ fn register_asset(deps: DepsMut, info: MessageInfo, staking_token: Addr) -> StdR }, )?; + match unbonding_period { + Some(unbonding_period) => { + store_unbonding_period(deps.storage, &asset_key, unbonding_period)?; + } + None => {} + } + Ok(Response::new().add_attributes([ ("action", "register_asset"), ("staking_token", staking_token.as_str()), + ( + "unbonding_period", + &unbonding_period.unwrap_or(0).to_string(), + ), ])) } diff --git a/contracts/oraiswap_staking/src/rewards.rs b/contracts/oraiswap_staking/src/rewards.rs index d964934f..673d5089 100644 --- a/contracts/oraiswap_staking/src/rewards.rs +++ b/contracts/oraiswap_staking/src/rewards.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use crate::contract::validate_migrate_store_status; use crate::state::{ read_config, read_is_migrated, read_pool_info, read_rewards_per_sec, rewards_read, - rewards_store, stakers_read, store_pool_info, PoolInfo, RewardInfo, + rewards_store, stakers_read, store_pool_info, PoolInfo, RewardInfo, DEFAULT_LIMIT, MAX_LIMIT, }; use cosmwasm_std::{ Addr, Api, CanonicalAddr, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, Response, @@ -13,9 +13,6 @@ use oraiswap::asset::{Asset, AssetRaw}; use oraiswap::querier::calc_range_start; use oraiswap::staking::{RewardInfoResponse, RewardInfoResponseItem, RewardMsg}; -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 30; - // deposit_reward must be from reward token contract pub fn deposit_reward( deps: DepsMut, diff --git a/contracts/oraiswap_staking/src/staking.rs b/contracts/oraiswap_staking/src/staking.rs index e23d5652..0ee67f6a 100644 --- a/contracts/oraiswap_staking/src/staking.rs +++ b/contracts/oraiswap_staking/src/staking.rs @@ -1,8 +1,10 @@ use crate::contract::validate_migrate_store_status; use crate::rewards::before_share_change; use crate::state::{ - read_config, read_is_migrated, read_pool_info, rewards_read, rewards_store, stakers_store, - store_is_migrated, store_pool_info, Config, PoolInfo, RewardInfo, + increase_unbonding_lock_id, read_all_user_to_lock_ids, read_config, read_is_migrated, + read_lock_info, read_pool_info, read_unbonding_period, remove_lock_info, rewards_read, + rewards_store, stakers_store, store_is_migrated, store_lock_info, store_pool_info, Config, + PoolInfo, RewardInfo, }; use cosmwasm_std::{ attr, to_binary, Addr, Api, CanonicalAddr, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, @@ -12,7 +14,7 @@ use cw20::Cw20ExecuteMsg; use oraiswap::asset::{Asset, AssetInfo, PairInfo}; use oraiswap::pair::ExecuteMsg as PairExecuteMsg; use oraiswap::querier::{query_pair_info, query_token_balance}; -use oraiswap::staking::ExecuteMsg; +use oraiswap::staking::{ExecuteMsg, LockInfo}; pub fn bond( deps: DepsMut, @@ -39,7 +41,7 @@ pub fn bond( pub fn unbond( deps: DepsMut, - _env: Env, + env: Env, staker_addr: Addr, staking_token: Addr, amount: Uint128, @@ -53,18 +55,9 @@ pub fn unbond( &staking_token, amount, )?; - let staking_token_addr = deps.api.addr_humanize(&staking_token)?; - let mut messages = vec![WasmMsg::Execute { - contract_addr: staking_token_addr.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: staker_addr.to_string(), - amount, - })?, - funds: vec![], - } - .into()]; + let mut messages = vec![]; // withdraw pending_withdraw assets (accumulated when changing reward_per_sec) messages.extend( reward_assets @@ -72,13 +65,34 @@ pub fn unbond( .map(|ra| Ok(ra.into_msg(None, &deps.querier, staker_addr.clone())?)) .collect::>>()?, ); - - Ok(Response::new().add_messages(messages).add_attributes([ - attr("action", "unbond"), - attr("staker_addr", staker_addr.as_str()), - attr("amount", &amount.to_string()), - attr("staking_token", staking_token_addr.as_str()), - ])) + // checking bonding period + if let Ok(period) = read_unbonding_period(deps.storage, staking_token_addr.as_bytes()) { + let lock_id = increase_unbonding_lock_id(deps.storage)?; + + let unlock_time = env.block.time.plus_seconds(period); + + store_lock_info( + deps.storage, + staking_token_addr.as_bytes(), + staker_addr.as_bytes(), + Uint128::from(lock_id), + LockInfo { + amount, + unlock_time, + }, + )?; + + Ok(Response::new().add_messages(messages).add_attributes([ + attr("action", "unbonding"), + attr("staker_addr", staker_addr.as_str()), + attr("amount", &amount.to_string()), + attr("staking_token", staking_token_addr.as_str()), + attr("lock_id", &lock_id.to_string()), + attr("unlock_time", &unlock_time.to_string()), + ])) + } else { + _unbond(staker_addr, staking_token_addr, amount, messages) + } } pub fn auto_stake( @@ -205,6 +219,74 @@ pub fn auto_stake_hook( bond(deps, staker_addr, staking_token, amount_to_stake) } +pub fn unbond_lock( + deps: DepsMut, + env: Env, + staker_addr: Addr, + staking_token: Addr, + lock_id: Option, +) -> StdResult { + match lock_id { + Some(lock_id) => { + let lock_info = read_lock_info( + deps.storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + Uint128::from(lock_id), + )?; + + if lock_info.unlock_time > env.block.time { + return Err(StdError::generic_err("Lock period has not expired yet")); + } + remove_lock_info( + deps.storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + Uint128::from(lock_id), + )?; + _unbond(staker_addr, staking_token, lock_info.amount, vec![]) + } + None => { + // execute 10 lock a time + let list_lock_info = read_all_user_to_lock_ids( + deps.storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + None, + None, + None, + ) + .map_err(|_| StdError::generic_err("No lock info"))?; + + let mut unbond_responses = vec![]; + let mut response = Response::new(); + + for (lock_id, lock_info) in list_lock_info { + if lock_info.unlock_time > env.block.time { + continue; + } + remove_lock_info( + deps.storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + lock_id, + )?; + unbond_responses.push(_unbond( + staker_addr.clone(), + staking_token.clone(), + lock_info.amount, + vec![], + )?); + } + + for res in unbond_responses.into_iter() { + response = response.add_attributes(res.clone().attributes); + } + Ok(response) + } + } +} + fn _increase_bond_amount( storage: &mut dyn Storage, api: &dyn Api, @@ -320,3 +402,29 @@ fn _decrease_bond_amount( Ok((staking_token, reward_assets)) } + +fn _unbond( + staker_addr: Addr, + staking_token_addr: Addr, + amount: Uint128, + reward_assets: Vec, +) -> StdResult { + let mut messages = vec![WasmMsg::Execute { + contract_addr: staking_token_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: staker_addr.to_string(), + amount, + })?, + funds: vec![], + } + .into()]; + + messages.extend(reward_assets); + + Ok(Response::new().add_messages(messages).add_attributes([ + attr("action", "unbond"), + attr("staker_addr", staker_addr.as_str()), + attr("amount", &amount.to_string()), + attr("staking_token", staking_token_addr.as_str()), + ])) +} diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index 4e0bc34c..2af8a75b 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; -use oraiswap::asset::AssetRaw; +use oraiswap::{asset::AssetRaw, querier::calc_range_start, staking::LockInfo}; -use cosmwasm_std::{CanonicalAddr, Decimal, StdResult, Storage, Uint128}; +use cosmwasm_std::{CanonicalAddr, Decimal, Order, StdResult, Storage, Uint128}; use cosmwasm_storage::{singleton, singleton_read, Bucket, ReadonlyBucket}; pub static KEY_CONFIG: &[u8] = b"config_v2"; @@ -13,6 +13,14 @@ pub static PREFIX_REWARDS_PER_SEC: &[u8] = b"rewards_per_sec_v3"; // a key to validate if we have finished migrating the store. Only allow staking functionalities when we have finished migrating pub static KEY_MIGRATE_STORE_CHECK: &[u8] = b"migrate_store_check"; +// Unbonded +pub static UNBONDING_PERIOD: &[u8] = b"unbonding_period"; +pub static LOCK_INFO: &[u8] = b"locking_users"; +pub static LOCK_ID: &[u8] = b"lock_id"; + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 30; + #[cw_serde] pub struct Config { pub owner: CanonicalAddr, @@ -138,3 +146,92 @@ pub fn read_rewards_per_sec(storage: &dyn Storage, asset_key: &[u8]) -> StdResul ReadonlyBucket::new(storage, PREFIX_REWARDS_PER_SEC); weight_bucket.load(asset_key) } + +pub fn store_unbonding_period( + storage: &mut dyn Storage, + asset_key: &[u8], + period: u64, +) -> StdResult<()> { + Bucket::new(storage, UNBONDING_PERIOD).save(asset_key, &period) +} + +pub fn read_unbonding_period(storage: &dyn Storage, asset_key: &[u8]) -> StdResult { + ReadonlyBucket::new(storage, UNBONDING_PERIOD).load(asset_key) +} + +pub fn increase_unbonding_lock_id(storage: &mut dyn Storage) -> StdResult { + let mut lock_id = singleton(storage, LOCK_ID); + let new_lock_id = lock_id.load().unwrap_or(0) + 1; + lock_id.save(&new_lock_id)?; + Ok(new_lock_id) +} + +pub fn read_unbonding_lock_id(storage: &dyn Storage) -> StdResult { + singleton_read(storage, LOCK_ID).load() +} + +pub fn store_lock_info( + storage: &mut dyn Storage, + asset_key: &[u8], + user: &[u8], + unbonding_order_id: Uint128, + lock_info: LockInfo, +) -> StdResult<()> { + let mut bucket = Bucket::multilevel(storage, &[LOCK_INFO, asset_key, user]); + bucket.save(&unbonding_order_id.to_be_bytes(), &lock_info) +} + +pub fn read_lock_info( + storage: &dyn Storage, + asset_key: &[u8], + user: &[u8], + unbonding_order_id: Uint128, +) -> StdResult { + ReadonlyBucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) + .load(&unbonding_order_id.to_be_bytes()) +} + +pub fn remove_lock_info( + storage: &mut dyn Storage, + asset_key: &[u8], + user: &[u8], + unbonding_order_id: Uint128, +) -> StdResult<()> { + Bucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) + .remove(&unbonding_order_id.to_be_bytes()); + Ok(()) +} + +pub fn read_all_user_to_lock_ids( + storage: &dyn Storage, + asset_key: &[u8], + user: &[u8], + start_after: Option, + limit: Option, + order: Option, +) -> StdResult> { + let order_by = Order::try_from(order.unwrap_or(1))?; + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + let (start, end) = match order_by { + Order::Ascending => ( + calc_range_start(start_after.map(|id| id.to_be_bytes().to_vec())), + None, + ), + Order::Descending => (None, start_after.map(|id| id.to_be_bytes().to_vec())), + }; + ReadonlyBucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) + .range(start.as_deref(), end.as_deref(), order_by) + .take(limit) + .map(|value| -> StdResult<(Uint128, LockInfo)> { + let (lock_id, lock_info) = value?; + Ok(( + (Uint128::from(u128::from_be_bytes(<[u8; 16]>::try_from(lock_id).map_err( + |_| cosmwasm_std::StdError::generic_err("Invalid unbonding order id"), + )?))), + lock_info, + )) + }) + .collect::>>() +} diff --git a/packages/oraiswap/src/staking.rs b/packages/oraiswap/src/staking.rs index f1543637..0a989534 100644 --- a/packages/oraiswap/src/staking.rs +++ b/packages/oraiswap/src/staking.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use crate::asset::{Asset, AssetInfo}; -use cosmwasm_std::{Addr, Binary, Decimal, Uint128}; +use cosmwasm_std::{Addr, Binary, Decimal, Timestamp, Uint128}; use cw20::Cw20ReceiveMsg; #[cw_serde] @@ -29,6 +29,7 @@ pub enum ExecuteMsg { }, RegisterAsset { staking_token: Addr, + unbonding_period: Option, }, DeprecateStakingToken { staking_token: Addr, @@ -74,6 +75,11 @@ pub enum ExecuteMsg { staker_addr: Addr, prev_staking_token_amount: Uint128, }, + /// Unbond lock + UnbondLock { + staking_token: Addr, + lock_id: Option, + }, } #[cw_serde] @@ -187,3 +193,9 @@ pub enum OldStoreType { IsMigrated { staker: String }, RewardsPerSec {}, } + +#[cw_serde] +pub struct LockInfo { + pub amount: Uint128, + pub unlock_time: Timestamp, +} From 18756873e96d0810c146144507ee92a7af566629 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Thu, 1 Feb 2024 17:16:03 +0700 Subject: [PATCH 02/13] chore: fix test --- contracts/oraiswap_staking/src/contract.rs | 7 +- .../src/testing/contract_test.rs | 3 + .../src/testing/deprecate_test.rs | 1 + .../src/testing/migrate_test.rs | 3 +- .../src/testing/reward_test.rs | 6 ++ .../src/testing/staking_test.rs | 102 +++++++++++++++++- 6 files changed, 113 insertions(+), 9 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index 7f76dce8..6e3c29a1 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -255,11 +255,8 @@ fn register_asset( }, )?; - match unbonding_period { - Some(unbonding_period) => { - store_unbonding_period(deps.storage, &asset_key, unbonding_period)?; - } - None => {} + if let Some(unbonding_period) = unbonding_period { + store_unbonding_period(deps.storage, staking_token.as_bytes(), unbonding_period)?; } Ok(Response::new().add_attributes([ diff --git a/contracts/oraiswap_staking/src/testing/contract_test.rs b/contracts/oraiswap_staking/src/testing/contract_test.rs index 7281a8cc..fea3c909 100644 --- a/contracts/oraiswap_staking/src/testing/contract_test.rs +++ b/contracts/oraiswap_staking/src/testing/contract_test.rs @@ -119,6 +119,7 @@ fn test_register() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; // failed with unauthorized error @@ -136,6 +137,7 @@ fn test_register() { vec![ attr("action", "register_asset"), attr("staking_token", "staking"), + attr("unbonding_period", "0"), ] ); @@ -193,6 +195,7 @@ fn test_query_staker_pagination() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); diff --git a/contracts/oraiswap_staking/src/testing/deprecate_test.rs b/contracts/oraiswap_staking/src/testing/deprecate_test.rs index 4b72f3b7..500b7d60 100644 --- a/contracts/oraiswap_staking/src/testing/deprecate_test.rs +++ b/contracts/oraiswap_staking/src/testing/deprecate_test.rs @@ -31,6 +31,7 @@ fn test_deprecate() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); diff --git a/contracts/oraiswap_staking/src/testing/migrate_test.rs b/contracts/oraiswap_staking/src/testing/migrate_test.rs index 7d94478c..f5762e01 100644 --- a/contracts/oraiswap_staking/src/testing/migrate_test.rs +++ b/contracts/oraiswap_staking/src/testing/migrate_test.rs @@ -210,7 +210,8 @@ fn test_validate_migrate_store_status_with_execute_msg() { mock_env(), owner.clone(), ExecuteMsg::RegisterAsset { - staking_token: empty_addr.clone() + staking_token: empty_addr.clone(), + unbonding_period: None } ), Err(StdError::generic_err( diff --git a/contracts/oraiswap_staking/src/testing/reward_test.rs b/contracts/oraiswap_staking/src/testing/reward_test.rs index 1a743190..8ed9d68e 100644 --- a/contracts/oraiswap_staking/src/testing/reward_test.rs +++ b/contracts/oraiswap_staking/src/testing/reward_test.rs @@ -54,6 +54,7 @@ fn test_deposit_reward() { let msg = ExecuteMsg::RegisterAsset { staking_token: staking_token.clone(), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -184,6 +185,7 @@ fn test_deposit_reward_when_no_bonding() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -302,6 +304,7 @@ fn test_before_share_changes() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -511,6 +514,7 @@ fn test_withdraw() { let msg = ExecuteMsg::RegisterAsset { staking_token: lp_addr.clone(), + unbonding_period: None, }; let _res = app @@ -621,6 +625,7 @@ fn test_update_rewards_per_sec() { let msg = ExecuteMsg::RegisterAsset { staking_token: staking_token.clone(), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -765,6 +770,7 @@ fn test_update_rewards_per_sec_with_multiple_bond() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 1a9f399c..63945dc6 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -82,6 +82,7 @@ fn test_bond_tokens() { let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -215,6 +216,7 @@ fn test_unbond() { // register asset let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), + unbonding_period: None, }; let info = mock_info("owner", &[]); @@ -348,11 +350,8 @@ fn test_unbond() { #[test] fn test_auto_stake() { let mut app = MockApp::new(&[(&"addr".to_string(), &[coin(10000000000u128, ORAI_DENOM)])]); - app.set_oracle_contract(Box::new(create_entry_points_testing!(oraiswap_oracle))); - app.set_token_contract(Box::new(create_entry_points_testing!(oraiswap_token))); - app.set_factory_and_pair_contract( Box::new( create_entry_points_testing!(oraiswap_factory) @@ -469,6 +468,7 @@ fn test_auto_stake() { let msg = ExecuteMsg::RegisterAsset { staking_token: pair_info.liquidity_token.clone(), + unbonding_period: None, }; let _res = app @@ -619,3 +619,99 @@ fn test_auto_stake() { } ); } + +#[test] +fn test_unbonding_period_register() { + let mut deps = mock_dependencies_with_balance(&[ + coin(10000000000u128, ORAI_DENOM), + coin(20000000000u128, ATOM_DENOM), + ]); + + let msg = InstantiateMsg { + owner: Some(Addr::unchecked("owner")), + rewarder: Addr::unchecked("rewarder"), + minter: Some(Addr::unchecked("mint")), + oracle_addr: Addr::unchecked("oracle"), + factory_addr: Addr::unchecked("factory"), + base_denom: None, + }; + + let info = mock_info("addr", &[]); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // will also add to the index the pending rewards from before the migration + let msg = ExecuteMsg::UpdateRewardsPerSec { + staking_token: Addr::unchecked("staking"), + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ATOM_DENOM.to_string(), + }, + amount: 200u128.into(), + }, + ], + }; + + let info = mock_info("owner", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // register asset + let msg = ExecuteMsg::RegisterAsset { + staking_token: Addr::unchecked("staking"), + unbonding_period: Some(100), + }; + + let info = mock_info("owner", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // bond 100 tokens + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: "addr".to_string(), + amount: Uint128::from(100u128), + msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), + }); + let info = mock_info("staking", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let msg = ExecuteMsg::DepositReward { + rewards: vec![RewardMsg { + staking_token: Addr::unchecked("staking"), + total_accumulation_amount: Uint128::from(300u128), + }], + }; + let info = mock_info("rewarder", &[]); + let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); + + // will also add to the index the pending rewards from before the migration + let msg = ExecuteMsg::UpdateRewardsPerSec { + staking_token: Addr::unchecked("staking"), + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ATOM_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + ], + }; + let info = mock_info("owner", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // unbond 150 tokens; failed + let msg = ExecuteMsg::Unbond { + staking_token: Addr::unchecked("staking"), + amount: Uint128::from(150u128), + }; +} From 481d632a24e065b004d0f81bdcdce4114f51072f Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Thu, 1 Feb 2024 18:25:32 +0700 Subject: [PATCH 03/13] feat: add query lock_infos and happycase --- contracts/oraiswap_staking/src/contract.rs | 47 ++++++++++- contracts/oraiswap_staking/src/staking.rs | 10 +-- .../src/testing/staking_test.rs | 81 +++++++++++++++++-- packages/oraiswap/src/staking.rs | 15 ++++ 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index 6e3c29a1..3cddedc3 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -12,8 +12,8 @@ use crate::rewards::{ }; use crate::staking::{auto_stake, auto_stake_hook, bond, unbond, unbond_lock}; use crate::state::{ - read_all_pool_infos, read_config, read_finish_migrate_store_status, read_pool_info, - read_rewards_per_sec, remove_pool_info, stakers_read, store_config, + read_all_pool_infos, read_all_user_to_lock_ids, read_config, read_finish_migrate_store_status, + read_pool_info, read_rewards_per_sec, remove_pool_info, stakers_read, store_config, store_finish_migrate_store_status, store_pool_info, store_rewards_per_sec, store_unbonding_period, Config, MigrationParams, PoolInfo, }; @@ -24,8 +24,8 @@ use cosmwasm_std::{ }; use oraiswap::asset::{Asset, AssetRaw, ORAI_DENOM}; use oraiswap::staking::{ - ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, OldStoreType, - PoolInfoResponse, QueryMsg, QueryPoolInfoResponse, RewardsPerSecResponse, + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfosResponse, MigrateMsg, + OldStoreType, PoolInfoResponse, QueryMsg, QueryPoolInfoResponse, RewardsPerSecResponse, }; use cw20::Cw20ReceiveMsg; @@ -344,10 +344,49 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { order, )?), QueryMsg::GetPoolsInformation {} => to_binary(&query_get_pools_infomation(deps)?), + QueryMsg::LockInfos { + staker_addr, + staking_token, + start_after, + limit, + order, + } => to_binary(&query_lock_infos( + deps, + _env, + staker_addr, + staking_token, + start_after, + limit, + order, + )?), QueryMsg::QueryOldStore { store_type } => query_old_store(deps, store_type), } } +pub fn query_lock_infos( + deps: Deps, + _env: Env, + staker_addr: Addr, + staking_token: Addr, + start_after: Option, + limit: Option, + order: Option, +) -> StdResult { + let lock_infos = read_all_user_to_lock_ids( + deps.storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + start_after, + limit, + order, + )?; + Ok(LockInfosResponse { + staker_addr, + staking_token, + lock_infos, + }) +} + pub fn query_config(deps: Deps) -> StdResult { let state = read_config(deps.storage)?; let resp = ConfigResponse { diff --git a/contracts/oraiswap_staking/src/staking.rs b/contracts/oraiswap_staking/src/staking.rs index 0ee67f6a..8f3313c3 100644 --- a/contracts/oraiswap_staking/src/staking.rs +++ b/contracts/oraiswap_staking/src/staking.rs @@ -62,7 +62,7 @@ pub fn unbond( messages.extend( reward_assets .into_iter() - .map(|ra| Ok(ra.into_msg(None, &deps.querier, staker_addr.clone())?)) + .map(|ra| ra.into_msg(None, &deps.querier, staker_addr.clone())) .collect::>>()?, ); // checking bonding period @@ -85,10 +85,10 @@ pub fn unbond( Ok(Response::new().add_messages(messages).add_attributes([ attr("action", "unbonding"), attr("staker_addr", staker_addr.as_str()), - attr("amount", &amount.to_string()), + attr("amount", amount.to_string()), attr("staking_token", staking_token_addr.as_str()), - attr("lock_id", &lock_id.to_string()), - attr("unlock_time", &unlock_time.to_string()), + attr("lock_id", lock_id.to_string()), + attr("unlock_time", unlock_time.to_string()), ])) } else { _unbond(staker_addr, staking_token_addr, amount, messages) @@ -424,7 +424,7 @@ fn _unbond( Ok(Response::new().add_messages(messages).add_attributes([ attr("action", "unbond"), attr("staker_addr", staker_addr.as_str()), - attr("amount", &amount.to_string()), + attr("amount", amount.to_string()), attr("staking_token", staking_token_addr.as_str()), ])) } diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 63945dc6..324a5eae 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -1,5 +1,6 @@ use crate::contract::{execute, instantiate, query, query_get_pools_infomation}; use crate::state::{store_pool_info, PoolInfo}; +use cosmwasm_schema::serde::Serialize; use cosmwasm_std::testing::{ mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, }; @@ -12,8 +13,8 @@ use oraiswap::asset::{Asset, AssetInfo, ORAI_DENOM}; use oraiswap::create_entry_points_testing; use oraiswap::pair::PairResponse; use oraiswap::staking::{ - Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolInfoResponse, QueryMsg, RewardInfoResponse, - RewardInfoResponseItem, RewardMsg, + Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfosResponse, PoolInfoResponse, QueryMsg, + RewardInfoResponse, RewardInfoResponseItem, RewardMsg, }; use oraiswap::testing::{AttributeUtil, MockApp, ATOM_DENOM}; @@ -621,7 +622,8 @@ fn test_auto_stake() { } #[test] -fn test_unbonding_period_register() { +fn test_unbonding_period_happy_case() { + let unbonding_period = 100; let mut deps = mock_dependencies_with_balance(&[ coin(10000000000u128, ORAI_DENOM), coin(20000000000u128, ATOM_DENOM), @@ -664,12 +666,20 @@ fn test_unbonding_period_register() { // register asset let msg = ExecuteMsg::RegisterAsset { staking_token: Addr::unchecked("staking"), - unbonding_period: Some(100), + unbonding_period: Some(unbonding_period), }; let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + res.attributes, + vec![ + attr("action", "register_asset"), + attr("staking_token", "staking"), + attr("unbonding_period", "100"), + ] + ); // bond 100 tokens let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { sender: "addr".to_string(), @@ -712,6 +722,65 @@ fn test_unbonding_period_register() { // unbond 150 tokens; failed let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), - amount: Uint128::from(150u128), + amount: Uint128::from(100u128), + }; + let info = mock_info("addr", &[]); + let mut unbond_env = mock_env(); + let _res = execute(deps.as_mut(), unbond_env.clone(), info.clone(), msg).unwrap(); + assert_eq!( + _res.attributes, + vec![ + attr("action", "unbonding"), + attr("staker_addr", "addr"), + attr("amount", Uint128::from(100u128).to_string()), + attr("staking_token", "staking"), + attr("lock_id", "1"), + attr( + "unlock_time", + unbond_env + .clone() + .block + .time + .plus_seconds(unbonding_period) + .to_string() + ), + ] + ); + + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); + let res = query( + deps.as_ref(), + unbond_env.clone(), + QueryMsg::LockInfos { + staker_addr: Addr::unchecked("addr"), + start_after: None, + limit: None, + order: None, + staking_token: Addr::unchecked("staking"), + }, + ) + .unwrap(); + let lock_ids = from_binary::(&res).unwrap(); + + // assert_eq!(lock_ids.lock_infos.len(), 1); + assert_eq!(lock_ids.lock_infos.len(), 1); + assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); + assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); + + let msg = ExecuteMsg::UnbondLock { + staking_token: Addr::unchecked("staking"), + lock_id: Some(1), }; + + let _res = execute(deps.as_mut(), unbond_env.clone(), info, msg).unwrap(); + + assert_eq!( + _res.attributes, + vec![ + attr("action", "unbond"), + attr("staker_addr", "addr"), + attr("amount", Uint128::from(100u128).to_string()), + attr("staking_token", "staking"), + ] + ); } diff --git a/packages/oraiswap/src/staking.rs b/packages/oraiswap/src/staking.rs index 0a989534..395c1294 100644 --- a/packages/oraiswap/src/staking.rs +++ b/packages/oraiswap/src/staking.rs @@ -127,6 +127,14 @@ pub enum QueryMsg { GetPoolsInformation {}, #[returns(Binary)] QueryOldStore { store_type: OldStoreType }, + #[returns(LockInfosResponse)] + LockInfos { + staker_addr: Addr, + start_after: Option, + limit: Option, + order: Option, + staking_token: Addr, + }, } // We define a custom struct for each query response @@ -199,3 +207,10 @@ pub struct LockInfo { pub amount: Uint128, pub unlock_time: Timestamp, } + +#[cw_serde] +pub struct LockInfosResponse { + pub staker_addr: Addr, + pub staking_token: Addr, + pub lock_infos: Vec<(Uint128, LockInfo)>, +} From 80803ff7c256a8c55d81af9fe7daff70e30b365c Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Fri, 2 Feb 2024 16:52:29 +0700 Subject: [PATCH 04/13] feat: add withdraw_lock to unbond function --- contracts/oraiswap_staking/src/contract.rs | 6 +- contracts/oraiswap_staking/src/staking.rs | 144 ++++++++++-------- .../src/testing/deprecate_test.rs | 8 +- .../src/testing/staking_test.rs | 33 ++-- packages/oraiswap/src/staking.rs | 5 - 5 files changed, 111 insertions(+), 85 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index 3cddedc3..8fd19d1f 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -10,7 +10,7 @@ use crate::rewards::{ deposit_reward, process_reward_assets, query_all_reward_infos, query_reward_info, withdraw_reward, withdraw_reward_others, }; -use crate::staking::{auto_stake, auto_stake_hook, bond, unbond, unbond_lock}; +use crate::staking::{auto_stake, auto_stake_hook, bond, unbond}; use crate::state::{ read_all_pool_infos, read_all_user_to_lock_ids, read_config, read_finish_migrate_store_status, read_pool_info, read_rewards_per_sec, remove_pool_info, stakers_read, store_config, @@ -103,10 +103,6 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S staker_addr, prev_staking_token_amount, ), - ExecuteMsg::UnbondLock { - staking_token, - lock_id, - } => unbond_lock(deps, env, info.sender, staking_token, lock_id), } } diff --git a/contracts/oraiswap_staking/src/staking.rs b/contracts/oraiswap_staking/src/staking.rs index 8f3313c3..e6264526 100644 --- a/contracts/oraiswap_staking/src/staking.rs +++ b/contracts/oraiswap_staking/src/staking.rs @@ -48,14 +48,16 @@ pub fn unbond( ) -> StdResult { validate_migrate_store_status(deps.storage)?; let staker_addr_raw: CanonicalAddr = deps.api.addr_canonicalize(staker_addr.as_str())?; - let (staking_token, reward_assets) = _decrease_bond_amount( + + let (staking_token_canonicalize, reward_assets) = _decrease_bond_amount( deps.storage, deps.api, &staker_addr_raw, &staking_token, amount, )?; - let staking_token_addr = deps.api.addr_humanize(&staking_token)?; + + let staking_token_addr = deps.api.addr_humanize(&staking_token_canonicalize)?; let mut messages = vec![]; // withdraw pending_withdraw assets (accumulated when changing reward_per_sec) @@ -65,34 +67,62 @@ pub fn unbond( .map(|ra| ra.into_msg(None, &deps.querier, staker_addr.clone())) .collect::>>()?, ); + let mut response = Response::new(); + + // withdraw_avaiable_lock + let withdraw_response = _withdraw_lock(deps.storage, &env, &staker_addr, &staking_token, None)?; + + messages.extend( + withdraw_response + .clone() + .messages + .into_iter() + .map(|msg| msg.msg) + .collect::>(), + ); + let withdraw_attrs = withdraw_response.attributes; + // checking bonding period if let Ok(period) = read_unbonding_period(deps.storage, staking_token_addr.as_bytes()) { - let lock_id = increase_unbonding_lock_id(deps.storage)?; - - let unlock_time = env.block.time.plus_seconds(period); - - store_lock_info( - deps.storage, - staking_token_addr.as_bytes(), - staker_addr.as_bytes(), - Uint128::from(lock_id), - LockInfo { - amount, - unlock_time, - }, - )?; - - Ok(Response::new().add_messages(messages).add_attributes([ - attr("action", "unbonding"), - attr("staker_addr", staker_addr.as_str()), - attr("amount", amount.to_string()), - attr("staking_token", staking_token_addr.as_str()), - attr("lock_id", lock_id.to_string()), - attr("unlock_time", unlock_time.to_string()), - ])) + if amount.gt(&Uint128::from(0u128)) { + let lock_id = increase_unbonding_lock_id(deps.storage)?; + + let unlock_time = env.block.time.plus_seconds(period); + + store_lock_info( + deps.storage, + staking_token_addr.as_bytes(), + staker_addr.as_bytes(), + Uint128::from(lock_id), + LockInfo { + amount, + unlock_time, + }, + )?; + + response = response.add_attributes([ + attr("action", "unbonding"), + attr("staker_addr", staker_addr.as_str()), + attr("amount", amount.to_string()), + attr("staking_token", staking_token_addr.as_str()), + attr("lock_id", lock_id.to_string()), + attr("unlock_time", unlock_time.to_string()), + ]) + } } else { - _unbond(staker_addr, staking_token_addr, amount, messages) + let unbond_response = _unbond(&staker_addr, &staking_token_addr, amount)?; + messages.extend( + unbond_response + .messages + .into_iter() + .map(|msg| msg.msg) + .collect::>(), + ); + response = response.add_attributes(unbond_response.attributes); } + Ok(response + .add_messages(messages) + .add_attributes(withdraw_attrs)) } pub fn auto_stake( @@ -219,17 +249,17 @@ pub fn auto_stake_hook( bond(deps, staker_addr, staking_token, amount_to_stake) } -pub fn unbond_lock( - deps: DepsMut, - env: Env, - staker_addr: Addr, - staking_token: Addr, +pub fn _withdraw_lock( + storage: &mut dyn Storage, + env: &Env, + staker_addr: &Addr, + staking_token: &Addr, lock_id: Option, ) -> StdResult { match lock_id { Some(lock_id) => { let lock_info = read_lock_info( - deps.storage, + storage, staking_token.as_bytes(), staker_addr.as_bytes(), Uint128::from(lock_id), @@ -239,21 +269,22 @@ pub fn unbond_lock( return Err(StdError::generic_err("Lock period has not expired yet")); } remove_lock_info( - deps.storage, + storage, staking_token.as_bytes(), staker_addr.as_bytes(), Uint128::from(lock_id), )?; - _unbond(staker_addr, staking_token, lock_info.amount, vec![]) + let response = _unbond(staker_addr, staking_token, lock_info.amount)?; + Ok(response.add_attribute("lock_id", lock_id.to_string())) } None => { // execute 10 lock a time let list_lock_info = read_all_user_to_lock_ids( - deps.storage, + storage, staking_token.as_bytes(), staker_addr.as_bytes(), None, - None, + Some(30), // take maxium 30 lock info None, ) .map_err(|_| StdError::generic_err("No lock info"))?; @@ -266,20 +297,24 @@ pub fn unbond_lock( continue; } remove_lock_info( - deps.storage, + storage, staking_token.as_bytes(), staker_addr.as_bytes(), lock_id, )?; - unbond_responses.push(_unbond( - staker_addr.clone(), - staking_token.clone(), - lock_info.amount, - vec![], - )?); + let unbond_response = _unbond(staker_addr, staking_token, lock_info.amount)?; + let unbond_response = unbond_response.add_attribute("lock_id", lock_id.to_string()); + unbond_responses.push(unbond_response); } for res in unbond_responses.into_iter() { + response = response.add_messages( + res.clone() + .messages + .into_iter() + .map(|sub_msg| sub_msg.msg) + .collect::>(), + ); response = response.add_attributes(res.clone().attributes); } Ok(response) @@ -386,16 +421,12 @@ fn _decrease_bond_amount( // if pending_withdraw is not empty, then return reward_assets to withdraw money reward_assets = reward_info .pending_withdraw - .into_iter() - .map(|ra| Ok(ra.to_normal(api)?)) + .iter() + .map(|ra| ra.to_normal(api)) .collect::>>()?; - - rewards_store(storage, staker_addr).remove(&asset_key); - // remove staker from the pool - stakers_store(storage, &asset_key).remove(staker_addr); - } else { - rewards_store(storage, staker_addr).save(&asset_key, &reward_info)?; + reward_info.pending_withdraw = vec![]; } + rewards_store(storage, staker_addr).save(&asset_key, &reward_info)?; // Update pool info store_pool_info(storage, &asset_key, &pool_info)?; @@ -403,13 +434,8 @@ fn _decrease_bond_amount( Ok((staking_token, reward_assets)) } -fn _unbond( - staker_addr: Addr, - staking_token_addr: Addr, - amount: Uint128, - reward_assets: Vec, -) -> StdResult { - let mut messages = vec![WasmMsg::Execute { +fn _unbond(staker_addr: &Addr, staking_token_addr: &Addr, amount: Uint128) -> StdResult { + let messages: Vec = vec![WasmMsg::Execute { contract_addr: staking_token_addr.to_string(), msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: staker_addr.to_string(), @@ -419,8 +445,6 @@ fn _unbond( } .into()]; - messages.extend(reward_assets); - Ok(Response::new().add_messages(messages).add_attributes([ attr("action", "unbond"), attr("staker_addr", staker_addr.as_str()), diff --git a/contracts/oraiswap_staking/src/testing/deprecate_test.rs b/contracts/oraiswap_staking/src/testing/deprecate_test.rs index 500b7d60..e0907a22 100644 --- a/contracts/oraiswap_staking/src/testing/deprecate_test.rs +++ b/contracts/oraiswap_staking/src/testing/deprecate_test.rs @@ -237,7 +237,13 @@ fn test_deprecate() { res, RewardInfoResponse { staker_addr: Addr::unchecked("addr"), - reward_infos: vec![], + reward_infos: vec![RewardInfoResponseItem { + staking_token: Addr::unchecked("new_staking"), + bond_amount: Uint128::from(0u128), + pending_reward: Uint128::from(0u128), + pending_withdraw: vec![], + should_migrate: None + }], } ); diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 324a5eae..55ba0b16 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -1,6 +1,5 @@ use crate::contract::{execute, instantiate, query, query_get_pools_infomation}; use crate::state::{store_pool_info, PoolInfo}; -use cosmwasm_schema::serde::Serialize; use cosmwasm_std::testing::{ mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, }; @@ -288,6 +287,14 @@ fn test_unbond() { assert_eq!( res.messages, vec![ + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(99u128, ORAI_DENOM)], + })), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(199u128, ATOM_DENOM)], + })), SubMsg::new(WasmMsg::Execute { contract_addr: "staking".to_string(), msg: to_binary(&Cw20ExecuteMsg::Transfer { @@ -297,14 +304,6 @@ fn test_unbond() { .unwrap(), funds: vec![], }), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: "addr".to_string(), - amount: vec![coin(99u128, ORAI_DENOM)], - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: "addr".to_string(), - amount: vec![coin(199u128, ATOM_DENOM)], - })) ] ); @@ -343,7 +342,13 @@ fn test_unbond() { res, RewardInfoResponse { staker_addr: Addr::unchecked("addr"), - reward_infos: vec![], + reward_infos: vec![RewardInfoResponseItem { + staking_token: Addr::unchecked("staking"), + bond_amount: Uint128::from(0u128), + pending_reward: Uint128::from(0u128), + pending_withdraw: vec![], + should_migrate: None + }], } ); } @@ -719,7 +724,6 @@ fn test_unbonding_period_happy_case() { let info = mock_info("owner", &[]); let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - // unbond 150 tokens; failed let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), amount: Uint128::from(100u128), @@ -767,11 +771,11 @@ fn test_unbonding_period_happy_case() { assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); - let msg = ExecuteMsg::UnbondLock { + let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), - lock_id: Some(1), + amount: Uint128::from(0u128), }; - + // let _res = execute(deps.as_mut(), unbond_env.clone(), info, msg).unwrap(); assert_eq!( @@ -781,6 +785,7 @@ fn test_unbonding_period_happy_case() { attr("staker_addr", "addr"), attr("amount", Uint128::from(100u128).to_string()), attr("staking_token", "staking"), + attr("lock_id", "1"), ] ); } diff --git a/packages/oraiswap/src/staking.rs b/packages/oraiswap/src/staking.rs index 395c1294..d0f19443 100644 --- a/packages/oraiswap/src/staking.rs +++ b/packages/oraiswap/src/staking.rs @@ -75,11 +75,6 @@ pub enum ExecuteMsg { staker_addr: Addr, prev_staking_token_amount: Uint128, }, - /// Unbond lock - UnbondLock { - staking_token: Addr, - lock_id: Option, - }, } #[cw_serde] From 4eef0876e3d4d4e58a3bdf376acd86ee5d62d591 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Fri, 2 Feb 2024 17:13:19 +0700 Subject: [PATCH 05/13] feat: unbond multiple timmes --- .../src/testing/staking_test.rs | 80 +++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 55ba0b16..dc76113c 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -726,7 +726,7 @@ fn test_unbonding_period_happy_case() { let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), - amount: Uint128::from(100u128), + amount: Uint128::from(50u128), }; let info = mock_info("addr", &[]); let mut unbond_env = mock_env(); @@ -736,7 +736,7 @@ fn test_unbonding_period_happy_case() { vec![ attr("action", "unbonding"), attr("staker_addr", "addr"), - attr("amount", Uint128::from(100u128).to_string()), + attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), attr("lock_id", "1"), attr( @@ -752,6 +752,7 @@ fn test_unbonding_period_happy_case() { ); unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); + let res = query( deps.as_ref(), unbond_env.clone(), @@ -768,24 +769,91 @@ fn test_unbonding_period_happy_case() { // assert_eq!(lock_ids.lock_infos.len(), 1); assert_eq!(lock_ids.lock_infos.len(), 1); + assert_eq!(lock_ids.lock_infos[0].0, Uint128::from(1u128)); assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), - amount: Uint128::from(0u128), + amount: Uint128::from(50u128), }; - // - let _res = execute(deps.as_mut(), unbond_env.clone(), info, msg).unwrap(); + let _res = execute(deps.as_mut(), unbond_env.clone(), info.clone(), msg).unwrap(); + let res = query( + deps.as_ref(), + unbond_env.clone(), + QueryMsg::LockInfos { + staker_addr: Addr::unchecked("addr"), + start_after: None, + limit: None, + order: None, + staking_token: Addr::unchecked("staking"), + }, + ) + .unwrap(); + let lock_ids = from_binary::(&res).unwrap(); + + assert_eq!(lock_ids.lock_infos.len(), 1); + assert_eq!(lock_ids.lock_infos[0].0, Uint128::from(2u128)); + assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); + assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); assert_eq!( _res.attributes, vec![ + attr("action", "unbonding"), + attr("staker_addr", "addr"), + attr("amount", Uint128::from(50u128).to_string()), + attr("staking_token", "staking"), + attr("lock_id", "2"), + attr( + "unlock_time", + unbond_env + .clone() + .block + .time + .plus_seconds(unbonding_period) + .to_string() + ), attr("action", "unbond"), attr("staker_addr", "addr"), - attr("amount", Uint128::from(100u128).to_string()), + attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), attr("lock_id", "1"), ] ); + + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); + + let msg = ExecuteMsg::Unbond { + staking_token: Addr::unchecked("staking"), + amount: Uint128::from(0u128), + }; + let _res = execute(deps.as_mut(), unbond_env.clone(), info, msg).unwrap(); + + let res = query( + deps.as_ref(), + unbond_env.clone(), + QueryMsg::LockInfos { + staker_addr: Addr::unchecked("addr"), + start_after: None, + limit: None, + order: None, + staking_token: Addr::unchecked("staking"), + }, + ) + .unwrap(); + + let lock_ids = from_binary::(&res).unwrap(); + assert_eq!(lock_ids.lock_infos.len(), 0); + + assert_eq!( + _res.attributes, + vec![ + attr("action", "unbond"), + attr("staker_addr", "addr"), + attr("amount", Uint128::from(50u128).to_string()), + attr("staking_token", "staking"), + attr("lock_id", "2"), + ] + ) } From 1256b927502a455315e46de73cd6321ee8f0962e Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Mon, 5 Feb 2024 12:04:43 +0700 Subject: [PATCH 06/13] fix: add if unbonding_period greater than 0 --- contracts/oraiswap_staking/src/contract.rs | 42 +++++++++------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index 8fd19d1f..f012ee9c 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -12,8 +12,8 @@ use crate::rewards::{ }; use crate::staking::{auto_stake, auto_stake_hook, bond, unbond}; use crate::state::{ - read_all_pool_infos, read_all_user_to_lock_ids, read_config, read_finish_migrate_store_status, - read_pool_info, read_rewards_per_sec, remove_pool_info, stakers_read, store_config, + read_all_pool_infos, read_config, read_finish_migrate_store_status, read_pool_info, + read_rewards_per_sec, read_user_lock_info, remove_pool_info, stakers_read, store_config, store_finish_migrate_store_status, store_pool_info, store_rewards_per_sec, store_unbonding_period, Config, MigrationParams, PoolInfo, }; @@ -24,8 +24,9 @@ use cosmwasm_std::{ }; use oraiswap::asset::{Asset, AssetRaw, ORAI_DENOM}; use oraiswap::staking::{ - ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfosResponse, MigrateMsg, - OldStoreType, PoolInfoResponse, QueryMsg, QueryPoolInfoResponse, RewardsPerSecResponse, + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfoResponse, LockInfosResponse, + MigrateMsg, OldStoreType, PoolInfoResponse, QueryMsg, QueryPoolInfoResponse, + RewardsPerSecResponse, }; use cw20::Cw20ReceiveMsg; @@ -252,7 +253,9 @@ fn register_asset( )?; if let Some(unbonding_period) = unbonding_period { - store_unbonding_period(deps.storage, staking_token.as_bytes(), unbonding_period)?; + if unbonding_period > 0 { + store_unbonding_period(deps.storage, staking_token.as_bytes(), unbonding_period)?; + } } Ok(Response::new().add_attributes([ @@ -343,18 +346,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::LockInfos { staker_addr, staking_token, - start_after, - limit, - order, - } => to_binary(&query_lock_infos( - deps, - _env, - staker_addr, - staking_token, - start_after, - limit, - order, - )?), + } => to_binary(&query_lock_infos(deps, _env, staker_addr, staking_token)?), QueryMsg::QueryOldStore { store_type } => query_old_store(deps, store_type), } } @@ -364,22 +356,22 @@ pub fn query_lock_infos( _env: Env, staker_addr: Addr, staking_token: Addr, - start_after: Option, - limit: Option, - order: Option, ) -> StdResult { - let lock_infos = read_all_user_to_lock_ids( + let lock_infos = read_user_lock_info( deps.storage, staking_token.as_bytes(), staker_addr.as_bytes(), - start_after, - limit, - order, )?; Ok(LockInfosResponse { staker_addr, staking_token, - lock_infos, + lock_infos: lock_infos + .into_iter() + .map(|lock| LockInfoResponse { + amount: lock.amount, + unlock_time: lock.unlock_time.seconds(), + }) + .collect(), }) } From caedea47c36d260b7e2b2947efbaceed26b06019 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Mon, 5 Feb 2024 12:06:58 +0700 Subject: [PATCH 07/13] feat: managing lock by an array of each token --- contracts/oraiswap_staking/src/staking.rs | 94 ++++------------ contracts/oraiswap_staking/src/state.rs | 124 ++++++++++------------ packages/oraiswap/src/staking.rs | 11 +- 3 files changed, 85 insertions(+), 144 deletions(-) diff --git a/contracts/oraiswap_staking/src/staking.rs b/contracts/oraiswap_staking/src/staking.rs index e6264526..4cef4741 100644 --- a/contracts/oraiswap_staking/src/staking.rs +++ b/contracts/oraiswap_staking/src/staking.rs @@ -1,10 +1,9 @@ use crate::contract::validate_migrate_store_status; use crate::rewards::before_share_change; use crate::state::{ - increase_unbonding_lock_id, read_all_user_to_lock_ids, read_config, read_is_migrated, - read_lock_info, read_pool_info, read_unbonding_period, remove_lock_info, rewards_read, - rewards_store, stakers_store, store_is_migrated, store_lock_info, store_pool_info, Config, - PoolInfo, RewardInfo, + insert_lock_info, read_config, read_is_migrated, read_pool_info, read_unbonding_period, + remove_and_accumulate_lock_info, rewards_read, rewards_store, stakers_store, store_is_migrated, + store_pool_info, Config, PoolInfo, RewardInfo, }; use cosmwasm_std::{ attr, to_binary, Addr, Api, CanonicalAddr, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, @@ -70,7 +69,7 @@ pub fn unbond( let mut response = Response::new(); // withdraw_avaiable_lock - let withdraw_response = _withdraw_lock(deps.storage, &env, &staker_addr, &staking_token, None)?; + let withdraw_response = _withdraw_lock(deps.storage, &env, &staker_addr, &staking_token)?; messages.extend( withdraw_response @@ -85,15 +84,11 @@ pub fn unbond( // checking bonding period if let Ok(period) = read_unbonding_period(deps.storage, staking_token_addr.as_bytes()) { if amount.gt(&Uint128::from(0u128)) { - let lock_id = increase_unbonding_lock_id(deps.storage)?; - let unlock_time = env.block.time.plus_seconds(period); - - store_lock_info( + insert_lock_info( deps.storage, staking_token_addr.as_bytes(), staker_addr.as_bytes(), - Uint128::from(lock_id), LockInfo { amount, unlock_time, @@ -105,8 +100,7 @@ pub fn unbond( attr("staker_addr", staker_addr.as_str()), attr("amount", amount.to_string()), attr("staking_token", staking_token_addr.as_str()), - attr("lock_id", lock_id.to_string()), - attr("unlock_time", unlock_time.to_string()), + attr("unlock_time", unlock_time.seconds().to_string()), ]) } } else { @@ -254,72 +248,22 @@ pub fn _withdraw_lock( env: &Env, staker_addr: &Addr, staking_token: &Addr, - lock_id: Option, ) -> StdResult { - match lock_id { - Some(lock_id) => { - let lock_info = read_lock_info( - storage, - staking_token.as_bytes(), - staker_addr.as_bytes(), - Uint128::from(lock_id), - )?; - - if lock_info.unlock_time > env.block.time { - return Err(StdError::generic_err("Lock period has not expired yet")); - } - remove_lock_info( - storage, - staking_token.as_bytes(), - staker_addr.as_bytes(), - Uint128::from(lock_id), - )?; - let response = _unbond(staker_addr, staking_token, lock_info.amount)?; - Ok(response.add_attribute("lock_id", lock_id.to_string())) - } - None => { - // execute 10 lock a time - let list_lock_info = read_all_user_to_lock_ids( - storage, - staking_token.as_bytes(), - staker_addr.as_bytes(), - None, - Some(30), // take maxium 30 lock info - None, - ) - .map_err(|_| StdError::generic_err("No lock info"))?; - - let mut unbond_responses = vec![]; - let mut response = Response::new(); - - for (lock_id, lock_info) in list_lock_info { - if lock_info.unlock_time > env.block.time { - continue; - } - remove_lock_info( - storage, - staking_token.as_bytes(), - staker_addr.as_bytes(), - lock_id, - )?; - let unbond_response = _unbond(staker_addr, staking_token, lock_info.amount)?; - let unbond_response = unbond_response.add_attribute("lock_id", lock_id.to_string()); - unbond_responses.push(unbond_response); - } + // execute 10 lock a time + let unlock_amount = remove_and_accumulate_lock_info( + storage, + staking_token.as_bytes(), + staker_addr.as_bytes(), + env.block.time, + )?; - for res in unbond_responses.into_iter() { - response = response.add_messages( - res.clone() - .messages - .into_iter() - .map(|sub_msg| sub_msg.msg) - .collect::>(), - ); - response = response.add_attributes(res.clone().attributes); - } - Ok(response) - } + if unlock_amount.is_zero() { + return Ok(Response::new()); } + + let unbond_response = _unbond(staker_addr, staking_token, unlock_amount)?; + + Ok(unbond_response) } fn _increase_bond_amount( diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index 2af8a75b..5f0b9fca 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; -use oraiswap::{asset::AssetRaw, querier::calc_range_start, staking::LockInfo}; +use oraiswap::{asset::AssetRaw, staking::LockInfo}; -use cosmwasm_std::{CanonicalAddr, Decimal, Order, StdResult, Storage, Uint128}; +use cosmwasm_std::{CanonicalAddr, Decimal, StdResult, Storage, Timestamp, Uint128}; use cosmwasm_storage::{singleton, singleton_read, Bucket, ReadonlyBucket}; pub static KEY_CONFIG: &[u8] = b"config_v2"; @@ -16,7 +16,6 @@ pub static KEY_MIGRATE_STORE_CHECK: &[u8] = b"migrate_store_check"; // Unbonded pub static UNBONDING_PERIOD: &[u8] = b"unbonding_period"; pub static LOCK_INFO: &[u8] = b"locking_users"; -pub static LOCK_ID: &[u8] = b"lock_id"; pub const DEFAULT_LIMIT: u32 = 10; pub const MAX_LIMIT: u32 = 30; @@ -159,79 +158,74 @@ pub fn read_unbonding_period(storage: &dyn Storage, asset_key: &[u8]) -> StdResu ReadonlyBucket::new(storage, UNBONDING_PERIOD).load(asset_key) } -pub fn increase_unbonding_lock_id(storage: &mut dyn Storage) -> StdResult { - let mut lock_id = singleton(storage, LOCK_ID); - let new_lock_id = lock_id.load().unwrap_or(0) + 1; - lock_id.save(&new_lock_id)?; - Ok(new_lock_id) -} - -pub fn read_unbonding_lock_id(storage: &dyn Storage) -> StdResult { - singleton_read(storage, LOCK_ID).load() -} - -pub fn store_lock_info( +pub fn insert_lock_info( storage: &mut dyn Storage, asset_key: &[u8], user: &[u8], - unbonding_order_id: Uint128, lock_info: LockInfo, ) -> StdResult<()> { - let mut bucket = Bucket::multilevel(storage, &[LOCK_INFO, asset_key, user]); - bucket.save(&unbonding_order_id.to_be_bytes(), &lock_info) + let mut lock_info_bucket = Bucket::multilevel(storage, &[LOCK_INFO, user]); + match lock_info_bucket.may_load(asset_key) { + Ok(Some(locks)) => { + let mut locks: Vec = locks; + if locks.len() == MAX_LIMIT as usize { + return Err(cosmwasm_std::StdError::generic_err( + "Exceed maximum limit of lock info", + )); + } + // append to front of vector + locks.insert(0, lock_info); + lock_info_bucket.save(asset_key, &locks); + } + Ok(None) => { + lock_info_bucket.save(asset_key, &vec![lock_info]); + } + Err(_) => { + return Err(cosmwasm_std::StdError::generic_err( + "Error while saving lock info", + )); + } + } + + Ok(()) } -pub fn read_lock_info( +pub fn read_user_lock_info( storage: &dyn Storage, asset_key: &[u8], user: &[u8], - unbonding_order_id: Uint128, -) -> StdResult { - ReadonlyBucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) - .load(&unbonding_order_id.to_be_bytes()) -} - -pub fn remove_lock_info( +) -> StdResult> { + match ReadonlyBucket::multilevel(storage, &[LOCK_INFO, user]).may_load(asset_key) { + Ok(Some(locks)) => Ok(locks), + Ok(None) => Ok(vec![]), + Err(_) => { + return Err(cosmwasm_std::StdError::generic_err( + "Error while saving lock info", + )); + } + } +} + +pub fn remove_and_accumulate_lock_info( storage: &mut dyn Storage, asset_key: &[u8], user: &[u8], - unbonding_order_id: Uint128, -) -> StdResult<()> { - Bucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) - .remove(&unbonding_order_id.to_be_bytes()); - Ok(()) -} - -pub fn read_all_user_to_lock_ids( - storage: &dyn Storage, - asset_key: &[u8], - user: &[u8], - start_after: Option, - limit: Option, - order: Option, -) -> StdResult> { - let order_by = Order::try_from(order.unwrap_or(1))?; - - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - - let (start, end) = match order_by { - Order::Ascending => ( - calc_range_start(start_after.map(|id| id.to_be_bytes().to_vec())), - None, - ), - Order::Descending => (None, start_after.map(|id| id.to_be_bytes().to_vec())), - }; - ReadonlyBucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]) - .range(start.as_deref(), end.as_deref(), order_by) - .take(limit) - .map(|value| -> StdResult<(Uint128, LockInfo)> { - let (lock_id, lock_info) = value?; - Ok(( - (Uint128::from(u128::from_be_bytes(<[u8; 16]>::try_from(lock_id).map_err( - |_| cosmwasm_std::StdError::generic_err("Invalid unbonding order id"), - )?))), - lock_info, - )) - }) - .collect::>>() + timestamp: Timestamp, +) -> StdResult { + let mut lock_info = read_user_lock_info(storage, asset_key, user)?; + let index = lock_info + .iter() + .position(|lock| lock.unlock_time < timestamp); + match index { + Some(index) => { + let mut bucket = Bucket::multilevel(storage, &[LOCK_INFO, user]); + let mut accumulated_amount = Uint128::zero(); + for lock in lock_info.drain(0..index + 1) { + accumulated_amount += lock.amount; + } + bucket.save(asset_key, &lock_info); + Ok(accumulated_amount) + } + None => Ok(Uint128::zero()), + } } diff --git a/packages/oraiswap/src/staking.rs b/packages/oraiswap/src/staking.rs index d0f19443..b0bb7494 100644 --- a/packages/oraiswap/src/staking.rs +++ b/packages/oraiswap/src/staking.rs @@ -125,9 +125,6 @@ pub enum QueryMsg { #[returns(LockInfosResponse)] LockInfos { staker_addr: Addr, - start_after: Option, - limit: Option, - order: Option, staking_token: Addr, }, } @@ -203,9 +200,15 @@ pub struct LockInfo { pub unlock_time: Timestamp, } +#[cw_serde] +pub struct LockInfoResponse { + pub amount: Uint128, + pub unlock_time: u64, +} + #[cw_serde] pub struct LockInfosResponse { pub staker_addr: Addr, pub staking_token: Addr, - pub lock_infos: Vec<(Uint128, LockInfo)>, + pub lock_infos: Vec, } From 8cd21163857f404ec57df63873a60315b4875a24 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Mon, 5 Feb 2024 12:07:49 +0700 Subject: [PATCH 08/13] feat: modify test follow new managing and add _setup_staking --- .../src/testing/staking_test.rs | 364 ++++++++---------- 1 file changed, 163 insertions(+), 201 deletions(-) diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index dc76113c..34f13751 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -1,11 +1,12 @@ use crate::contract::{execute, instantiate, query, query_get_pools_infomation}; use crate::state::{store_pool_info, PoolInfo}; use cosmwasm_std::testing::{ - mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockApi, MockQuerier, + MockStorage, }; use cosmwasm_std::{ - attr, coin, from_binary, to_binary, Addr, Api, BankMsg, Coin, CosmosMsg, Decimal, StdError, - SubMsg, Uint128, WasmMsg, + attr, coin, from_binary, to_binary, Addr, Api, BankMsg, Coin, CosmosMsg, Decimal, OwnedDeps, + StdError, SubMsg, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use oraiswap::asset::{Asset, AssetInfo, ORAI_DENOM}; @@ -175,91 +176,7 @@ fn test_bond_tokens() { #[test] fn test_unbond() { - let mut deps = mock_dependencies_with_balance(&[ - coin(10000000000u128, ORAI_DENOM), - coin(20000000000u128, ATOM_DENOM), - ]); - - let msg = InstantiateMsg { - owner: Some(Addr::unchecked("owner")), - rewarder: Addr::unchecked("rewarder"), - minter: Some(Addr::unchecked("mint")), - oracle_addr: Addr::unchecked("oracle"), - factory_addr: Addr::unchecked("factory"), - base_denom: None, - }; - - let info = mock_info("addr", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // will also add to the index the pending rewards from before the migration - let msg = ExecuteMsg::UpdateRewardsPerSec { - staking_token: Addr::unchecked("staking"), - assets: vec![ - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ATOM_DENOM.to_string(), - }, - amount: 200u128.into(), - }, - ], - }; - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // register asset - let msg = ExecuteMsg::RegisterAsset { - staking_token: Addr::unchecked("staking"), - unbonding_period: None, - }; - - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // bond 100 tokens - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "addr".to_string(), - amount: Uint128::from(100u128), - msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), - }); - let info = mock_info("staking", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let msg = ExecuteMsg::DepositReward { - rewards: vec![RewardMsg { - staking_token: Addr::unchecked("staking"), - total_accumulation_amount: Uint128::from(300u128), - }], - }; - let info = mock_info("rewarder", &[]); - let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); - - // will also add to the index the pending rewards from before the migration - let msg = ExecuteMsg::UpdateRewardsPerSec { - staking_token: Addr::unchecked("staking"), - assets: vec![ - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ATOM_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - ], - }; - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let mut deps = _setup_staking(None); // unbond 150 tokens; failed let msg = ExecuteMsg::Unbond { @@ -629,100 +546,7 @@ fn test_auto_stake() { #[test] fn test_unbonding_period_happy_case() { let unbonding_period = 100; - let mut deps = mock_dependencies_with_balance(&[ - coin(10000000000u128, ORAI_DENOM), - coin(20000000000u128, ATOM_DENOM), - ]); - - let msg = InstantiateMsg { - owner: Some(Addr::unchecked("owner")), - rewarder: Addr::unchecked("rewarder"), - minter: Some(Addr::unchecked("mint")), - oracle_addr: Addr::unchecked("oracle"), - factory_addr: Addr::unchecked("factory"), - base_denom: None, - }; - - let info = mock_info("addr", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // will also add to the index the pending rewards from before the migration - let msg = ExecuteMsg::UpdateRewardsPerSec { - staking_token: Addr::unchecked("staking"), - assets: vec![ - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ATOM_DENOM.to_string(), - }, - amount: 200u128.into(), - }, - ], - }; - - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // register asset - let msg = ExecuteMsg::RegisterAsset { - staking_token: Addr::unchecked("staking"), - unbonding_period: Some(unbonding_period), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - res.attributes, - vec![ - attr("action", "register_asset"), - attr("staking_token", "staking"), - attr("unbonding_period", "100"), - ] - ); - // bond 100 tokens - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "addr".to_string(), - amount: Uint128::from(100u128), - msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), - }); - let info = mock_info("staking", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let msg = ExecuteMsg::DepositReward { - rewards: vec![RewardMsg { - staking_token: Addr::unchecked("staking"), - total_accumulation_amount: Uint128::from(300u128), - }], - }; - let info = mock_info("rewarder", &[]); - let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); - - // will also add to the index the pending rewards from before the migration - let msg = ExecuteMsg::UpdateRewardsPerSec { - staking_token: Addr::unchecked("staking"), - assets: vec![ - Asset { - info: AssetInfo::NativeToken { - denom: ORAI_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - Asset { - info: AssetInfo::NativeToken { - denom: ATOM_DENOM.to_string(), - }, - amount: 100u128.into(), - }, - ], - }; - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let mut deps = _setup_staking(Some(unbonding_period)); let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), @@ -730,7 +554,9 @@ fn test_unbonding_period_happy_case() { }; let info = mock_info("addr", &[]); let mut unbond_env = mock_env(); + let _res = execute(deps.as_mut(), unbond_env.clone(), info.clone(), msg).unwrap(); + assert_eq!( _res.attributes, vec![ @@ -738,7 +564,6 @@ fn test_unbonding_period_happy_case() { attr("staker_addr", "addr"), attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), - attr("lock_id", "1"), attr( "unlock_time", unbond_env @@ -746,33 +571,40 @@ fn test_unbonding_period_happy_case() { .block .time .plus_seconds(unbonding_period) + .seconds() .to_string() ), ] ); - unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); - let res = query( deps.as_ref(), unbond_env.clone(), QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), - start_after: None, - limit: None, - order: None, staking_token: Addr::unchecked("staking"), }, ) .unwrap(); let lock_ids = from_binary::(&res).unwrap(); - // assert_eq!(lock_ids.lock_infos.len(), 1); assert_eq!(lock_ids.lock_infos.len(), 1); - assert_eq!(lock_ids.lock_infos[0].0, Uint128::from(1u128)); + assert_eq!(lock_ids.lock_infos[0].amount, Uint128::from(50u128)); + assert_eq!( + lock_ids.lock_infos[0].unlock_time, + unbond_env + .clone() + .block + .time + .plus_seconds(unbonding_period) + .seconds() + ); assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); + // increase block.time + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); + // Unbond and withdraw_lock let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), amount: Uint128::from(50u128), @@ -784,17 +616,12 @@ fn test_unbonding_period_happy_case() { unbond_env.clone(), QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), - start_after: None, - limit: None, - order: None, staking_token: Addr::unchecked("staking"), }, ) .unwrap(); let lock_ids = from_binary::(&res).unwrap(); - assert_eq!(lock_ids.lock_infos.len(), 1); - assert_eq!(lock_ids.lock_infos[0].0, Uint128::from(2u128)); assert_eq!(lock_ids.staking_token, Addr::unchecked("staking")); assert_eq!(lock_ids.staker_addr, Addr::unchecked("addr")); assert_eq!( @@ -804,7 +631,6 @@ fn test_unbonding_period_happy_case() { attr("staker_addr", "addr"), attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), - attr("lock_id", "2"), attr( "unlock_time", unbond_env @@ -812,13 +638,35 @@ fn test_unbonding_period_happy_case() { .block .time .plus_seconds(unbonding_period) + .seconds() .to_string() ), attr("action", "unbond"), attr("staker_addr", "addr"), attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), - attr("lock_id", "1"), + ] + ); + assert_eq!( + _res.messages, + vec![ + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(99u128, ORAI_DENOM)], + })), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(199u128, ATOM_DENOM)], + })), + SubMsg::new(WasmMsg::Execute { + contract_addr: "staking".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "addr".to_string(), + amount: Uint128::from(50u128), + }) + .unwrap(), + funds: vec![], + }), ] ); @@ -835,9 +683,6 @@ fn test_unbonding_period_happy_case() { unbond_env.clone(), QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), - start_after: None, - limit: None, - order: None, staking_token: Addr::unchecked("staking"), }, ) @@ -853,7 +698,124 @@ fn test_unbonding_period_happy_case() { attr("staker_addr", "addr"), attr("amount", Uint128::from(50u128).to_string()), attr("staking_token", "staking"), - attr("lock_id", "2"), ] + ); + assert_eq!( + _res.messages, + vec![SubMsg::new(WasmMsg::Execute { + contract_addr: "staking".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "addr".to_string(), + amount: Uint128::from(50u128), + }) + .unwrap(), + funds: vec![], + }),] ) } + +#[test] +pub fn test_max_exceed_number_lock() { + let unbonding_period = 100; + let mut deps = _setup_staking(Some(unbonding_period)); +} + +fn _setup_staking(unbonding_period: Option) -> OwnedDeps { + let mut deps = mock_dependencies_with_balance(&[ + coin(10000000000u128, ORAI_DENOM), + coin(20000000000u128, ATOM_DENOM), + ]); + let msg = InstantiateMsg { + owner: Some(Addr::unchecked("owner")), + rewarder: Addr::unchecked("rewarder"), + minter: Some(Addr::unchecked("mint")), + oracle_addr: Addr::unchecked("oracle"), + factory_addr: Addr::unchecked("factory"), + base_denom: None, + }; + + let info = mock_info("addr", &[]); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // will also add to the index the pending rewards from before the migration + let msg = ExecuteMsg::UpdateRewardsPerSec { + staking_token: Addr::unchecked("staking"), + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ATOM_DENOM.to_string(), + }, + amount: 200u128.into(), + }, + ], + }; + + let info = mock_info("owner", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // register asset + let msg = ExecuteMsg::RegisterAsset { + staking_token: Addr::unchecked("staking"), + unbonding_period, + }; + + let info = mock_info("owner", &[]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("action", "register_asset"), + attr("staking_token", "staking"), + attr( + "unbonding_period", + unbonding_period.unwrap_or(0).to_string() + ) + ] + ); + // bond 100 tokens + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: "addr".to_string(), + amount: Uint128::from(100u128), + msg: to_binary(&Cw20HookMsg::Bond {}).unwrap(), + }); + let info = mock_info("staking", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let msg = ExecuteMsg::DepositReward { + rewards: vec![RewardMsg { + staking_token: Addr::unchecked("staking"), + total_accumulation_amount: Uint128::from(300u128), + }], + }; + let info = mock_info("rewarder", &[]); + let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); + + // will also add to the index the pending rewards from before the migration + let msg = ExecuteMsg::UpdateRewardsPerSec { + staking_token: Addr::unchecked("staking"), + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: ORAI_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: ATOM_DENOM.to_string(), + }, + amount: 100u128.into(), + }, + ], + }; + let info = mock_info("owner", &[]); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + deps +} From 89a3698d5a4d27ddf25c99686afe894b7c1af6d0 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Mon, 5 Feb 2024 13:58:29 +0700 Subject: [PATCH 09/13] feat: add test max exceed number of lock --- contracts/oraiswap_staking/src/state.rs | 4 +- .../src/testing/staking_test.rs | 78 ++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index 5f0b9fca..cb0a0cf3 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -175,10 +175,10 @@ pub fn insert_lock_info( } // append to front of vector locks.insert(0, lock_info); - lock_info_bucket.save(asset_key, &locks); + let _ = lock_info_bucket.save(asset_key, &locks); } Ok(None) => { - lock_info_bucket.save(asset_key, &vec![lock_info]); + let _ = lock_info_bucket.save(asset_key, &vec![lock_info]); } Err(_) => { return Err(cosmwasm_std::StdError::generic_err( diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 34f13751..af2fe6e9 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -1,5 +1,5 @@ use crate::contract::{execute, instantiate, query, query_get_pools_infomation}; -use crate::state::{store_pool_info, PoolInfo}; +use crate::state::{store_pool_info, PoolInfo, MAX_LIMIT}; use cosmwasm_std::testing::{ mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockApi, MockQuerier, MockStorage, @@ -716,8 +716,82 @@ fn test_unbonding_period_happy_case() { #[test] pub fn test_max_exceed_number_lock() { - let unbonding_period = 100; + let unbonding_period = 10000; let mut deps = _setup_staking(Some(unbonding_period)); + let info = mock_info("addr", &[]); + let mut unbond_env = mock_env(); + + for i in 0..MAX_LIMIT { + let msg = ExecuteMsg::Unbond { + staking_token: Addr::unchecked("staking"), + amount: Uint128::from(1u128), + }; + let mut clone_unbonded = unbond_env.clone(); + clone_unbonded.block.time = clone_unbonded + .block + .time + .plus_seconds((i as u64) * unbonding_period / 50); + let _res = execute(deps.as_mut(), clone_unbonded, info.clone(), msg).unwrap(); + } + let binary_response = query( + deps.as_ref(), + unbond_env.clone(), + QueryMsg::LockInfos { + staker_addr: Addr::unchecked("addr"), + staking_token: Addr::unchecked("staking"), + }, + ) + .unwrap(); + let lock_infos = from_binary::(&binary_response).unwrap(); + assert_eq!(lock_infos.lock_infos.len(), MAX_LIMIT as usize); + + let msg = ExecuteMsg::Unbond { + staking_token: Addr::unchecked("staking"), + amount: Uint128::from(1u128), + }; + let mut clone_unbonded = unbond_env.clone(); + clone_unbonded.block.time = clone_unbonded + .block + .time + .plus_seconds(((MAX_LIMIT + 1) as u64) * unbonding_period / 50); + let res = execute(deps.as_mut(), clone_unbonded, info.clone(), msg).unwrap_err(); + if let StdError::GenericErr { msg, .. } = res { + assert_eq!(msg, "Exceed maximum limit of lock info"); + } else { + panic!("Must return generic error"); + } + + // skip to time have unlock all the lock_info + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); + let msg = ExecuteMsg::Unbond { + staking_token: Addr::unchecked("staking"), + amount: Uint128::from(0u128), + }; + + let res = execute(deps.as_mut(), unbond_env, info.clone(), msg).unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("action", "unbond"), + attr("staker_addr", "addr"), + attr("amount", Uint128::from(MAX_LIMIT as u128).to_string()), + attr("staking_token", "staking"), + ] + ); + + assert_eq!( + res.messages, + vec![SubMsg::new(WasmMsg::Execute { + contract_addr: "staking".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: "addr".to_string(), + amount: Uint128::from(MAX_LIMIT as u128), + }) + .unwrap(), + funds: vec![], + }),] + ) } fn _setup_staking(unbonding_period: Option) -> OwnedDeps { From 2aa85bb69c52a6bd8937ba878659d6472bb2da5a Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Mon, 5 Feb 2024 15:04:34 +0700 Subject: [PATCH 10/13] chore: return error when read lock info --- contracts/oraiswap_staking/src/state.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index cb0a0cf3..87a06e57 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -198,11 +198,9 @@ pub fn read_user_lock_info( match ReadonlyBucket::multilevel(storage, &[LOCK_INFO, user]).may_load(asset_key) { Ok(Some(locks)) => Ok(locks), Ok(None) => Ok(vec![]), - Err(_) => { - return Err(cosmwasm_std::StdError::generic_err( - "Error while saving lock info", - )); - } + Err(_) => Err(cosmwasm_std::StdError::generic_err( + "Error while read lock info", + )), } } From 4f712863973ea513ffc5422dad276dd79ed75219 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 5 Feb 2024 16:59:14 -0800 Subject: [PATCH 11/13] fix: add ? when remove & accumulate lock info --- contracts/oraiswap_staking/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index 87a06e57..f7ba6ed1 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -221,7 +221,7 @@ pub fn remove_and_accumulate_lock_info( for lock in lock_info.drain(0..index + 1) { accumulated_amount += lock.amount; } - bucket.save(asset_key, &lock_info); + bucket.save(asset_key, &lock_info)?; Ok(accumulated_amount) } None => Ok(Uint128::zero()), From fe21585286b3be8f7d1878eac5180df6cae49269 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 5 Feb 2024 17:22:51 -0800 Subject: [PATCH 12/13] fix: only decrease bond, lock & unbond when amount != 0 --- contracts/oraiswap_staking/src/staking.rs | 64 +++++++++---------- .../src/testing/staking_test.rs | 16 ++--- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/contracts/oraiswap_staking/src/staking.rs b/contracts/oraiswap_staking/src/staking.rs index 4cef4741..dbbcdc93 100644 --- a/contracts/oraiswap_staking/src/staking.rs +++ b/contracts/oraiswap_staking/src/staking.rs @@ -47,25 +47,7 @@ pub fn unbond( ) -> StdResult { validate_migrate_store_status(deps.storage)?; let staker_addr_raw: CanonicalAddr = deps.api.addr_canonicalize(staker_addr.as_str())?; - - let (staking_token_canonicalize, reward_assets) = _decrease_bond_amount( - deps.storage, - deps.api, - &staker_addr_raw, - &staking_token, - amount, - )?; - - let staking_token_addr = deps.api.addr_humanize(&staking_token_canonicalize)?; - let mut messages = vec![]; - // withdraw pending_withdraw assets (accumulated when changing reward_per_sec) - messages.extend( - reward_assets - .into_iter() - .map(|ra| ra.into_msg(None, &deps.querier, staker_addr.clone())) - .collect::>>()?, - ); let mut response = Response::new(); // withdraw_avaiable_lock @@ -79,15 +61,29 @@ pub fn unbond( .map(|msg| msg.msg) .collect::>(), ); - let withdraw_attrs = withdraw_response.attributes; - // checking bonding period - if let Ok(period) = read_unbonding_period(deps.storage, staking_token_addr.as_bytes()) { - if amount.gt(&Uint128::from(0u128)) { + let withdraw_attrs = withdraw_response.attributes; + if !amount.is_zero() { + let (_, reward_assets) = _decrease_bond_amount( + deps.storage, + deps.api, + &staker_addr_raw, + &staking_token, + amount, + )?; + // withdraw pending_withdraw assets (accumulated when changing reward_per_sec) + messages.extend( + reward_assets + .into_iter() + .map(|ra| ra.into_msg(None, &deps.querier, staker_addr.clone())) + .collect::>>()?, + ); + // checking bonding period + if let Ok(period) = read_unbonding_period(deps.storage, staking_token.as_bytes()) { let unlock_time = env.block.time.plus_seconds(period); insert_lock_info( deps.storage, - staking_token_addr.as_bytes(), + staking_token.as_bytes(), staker_addr.as_bytes(), LockInfo { amount, @@ -99,20 +95,20 @@ pub fn unbond( attr("action", "unbonding"), attr("staker_addr", staker_addr.as_str()), attr("amount", amount.to_string()), - attr("staking_token", staking_token_addr.as_str()), + attr("staking_token", staking_token.as_str()), attr("unlock_time", unlock_time.seconds().to_string()), ]) + } else { + let unbond_response = _unbond(&staker_addr, &staking_token, amount)?; + messages.extend( + unbond_response + .messages + .into_iter() + .map(|msg| msg.msg) + .collect::>(), + ); + response = response.add_attributes(unbond_response.attributes); } - } else { - let unbond_response = _unbond(&staker_addr, &staking_token_addr, amount)?; - messages.extend( - unbond_response - .messages - .into_iter() - .map(|msg| msg.msg) - .collect::>(), - ); - response = response.add_attributes(unbond_response.attributes); } Ok(response .add_messages(messages) diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index af2fe6e9..2ed2d209 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -650,14 +650,6 @@ fn test_unbonding_period_happy_case() { assert_eq!( _res.messages, vec![ - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: "addr".to_string(), - amount: vec![coin(99u128, ORAI_DENOM)], - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: "addr".to_string(), - amount: vec![coin(199u128, ATOM_DENOM)], - })), SubMsg::new(WasmMsg::Execute { contract_addr: "staking".to_string(), msg: to_binary(&Cw20ExecuteMsg::Transfer { @@ -667,6 +659,14 @@ fn test_unbonding_period_happy_case() { .unwrap(), funds: vec![], }), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(99u128, ORAI_DENOM)], + })), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "addr".to_string(), + amount: vec![coin(199u128, ATOM_DENOM)], + })), ] ); From fde53d2812a2e4a5b8866f1ca66265d296160963 Mon Sep 17 00:00:00 2001 From: meomeocoj Date: Tue, 6 Feb 2024 16:26:49 +0700 Subject: [PATCH 13/13] feat: using timestamp as key in lock_info --- contracts/oraiswap_staking/src/contract.rs | 19 +++- contracts/oraiswap_staking/src/state.rs | 102 ++++++++++-------- .../src/testing/staking_test.rs | 54 ++++++---- packages/oraiswap/src/staking.rs | 4 + 4 files changed, 112 insertions(+), 67 deletions(-) diff --git a/contracts/oraiswap_staking/src/contract.rs b/contracts/oraiswap_staking/src/contract.rs index f012ee9c..5b5a83b4 100644 --- a/contracts/oraiswap_staking/src/contract.rs +++ b/contracts/oraiswap_staking/src/contract.rs @@ -346,7 +346,18 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::LockInfos { staker_addr, staking_token, - } => to_binary(&query_lock_infos(deps, _env, staker_addr, staking_token)?), + start_after, + limit, + order, + } => to_binary(&query_lock_infos( + deps, + _env, + staker_addr, + staking_token, + start_after, + limit, + order, + )?), QueryMsg::QueryOldStore { store_type } => query_old_store(deps, store_type), } } @@ -356,11 +367,17 @@ pub fn query_lock_infos( _env: Env, staker_addr: Addr, staking_token: Addr, + start_after: Option, + limit: Option, + order: Option, ) -> StdResult { let lock_infos = read_user_lock_info( deps.storage, staking_token.as_bytes(), staker_addr.as_bytes(), + start_after, + limit, + order, )?; Ok(LockInfosResponse { staker_addr, diff --git a/contracts/oraiswap_staking/src/state.rs b/contracts/oraiswap_staking/src/state.rs index f7ba6ed1..cd237812 100644 --- a/contracts/oraiswap_staking/src/state.rs +++ b/contracts/oraiswap_staking/src/state.rs @@ -1,7 +1,9 @@ use cosmwasm_schema::cw_serde; -use oraiswap::{asset::AssetRaw, staking::LockInfo}; +use oraiswap::{asset::AssetRaw, querier::calc_range_start, staking::LockInfo}; -use cosmwasm_std::{CanonicalAddr, Decimal, StdResult, Storage, Timestamp, Uint128}; +use cosmwasm_std::{ + CanonicalAddr, Decimal, Order, StdError, StdResult, Storage, Timestamp, Uint128, +}; use cosmwasm_storage::{singleton, singleton_read, Bucket, ReadonlyBucket}; pub static KEY_CONFIG: &[u8] = b"config_v2"; @@ -164,44 +166,44 @@ pub fn insert_lock_info( user: &[u8], lock_info: LockInfo, ) -> StdResult<()> { - let mut lock_info_bucket = Bucket::multilevel(storage, &[LOCK_INFO, user]); - match lock_info_bucket.may_load(asset_key) { - Ok(Some(locks)) => { - let mut locks: Vec = locks; - if locks.len() == MAX_LIMIT as usize { - return Err(cosmwasm_std::StdError::generic_err( - "Exceed maximum limit of lock info", - )); - } - // append to front of vector - locks.insert(0, lock_info); - let _ = lock_info_bucket.save(asset_key, &locks); - } - Ok(None) => { - let _ = lock_info_bucket.save(asset_key, &vec![lock_info]); - } - Err(_) => { - return Err(cosmwasm_std::StdError::generic_err( - "Error while saving lock info", - )); - } - } - - Ok(()) + Bucket::multilevel(storage, &[LOCK_INFO, asset_key, user]).save( + &lock_info.unlock_time.seconds().to_be_bytes(), + &lock_info.amount, + ) } pub fn read_user_lock_info( storage: &dyn Storage, asset_key: &[u8], user: &[u8], + start_after: Option, + limit: Option, + order: Option, ) -> StdResult> { - match ReadonlyBucket::multilevel(storage, &[LOCK_INFO, user]).may_load(asset_key) { - Ok(Some(locks)) => Ok(locks), - Ok(None) => Ok(vec![]), - Err(_) => Err(cosmwasm_std::StdError::generic_err( - "Error while read lock info", - )), - } + let order_by = Order::try_from(order.unwrap_or(1))?; + + let start_after = start_after.map(|a| a.to_be_bytes().to_vec()); + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + let (start, end) = match order_by { + Order::Ascending => (calc_range_start(start_after), None), + Order::Descending => (None, start_after), + }; + ReadonlyBucket::multilevel(storage, &[LOCK_INFO, asset_key, user]) + .range(start.as_deref(), end.as_deref(), order_by) + .take(limit) + .map(|item| { + let (time, amount) = item?; + Ok(LockInfo { + unlock_time: Timestamp::from_seconds(u64::from_be_bytes( + <[u8; 8]>::try_from(time) + .map_err(|_| StdError::generic_err("Casting u64 to timestamp fail"))?, + )), + amount, + }) + }) + .collect() } pub fn remove_and_accumulate_lock_info( @@ -210,20 +212,26 @@ pub fn remove_and_accumulate_lock_info( user: &[u8], timestamp: Timestamp, ) -> StdResult { - let mut lock_info = read_user_lock_info(storage, asset_key, user)?; - let index = lock_info - .iter() - .position(|lock| lock.unlock_time < timestamp); - match index { - Some(index) => { - let mut bucket = Bucket::multilevel(storage, &[LOCK_INFO, user]); - let mut accumulated_amount = Uint128::zero(); - for lock in lock_info.drain(0..index + 1) { - accumulated_amount += lock.amount; - } - bucket.save(asset_key, &lock_info)?; - Ok(accumulated_amount) + let mut bucket = Bucket::::multilevel(storage, &[LOCK_INFO, asset_key, user]); + let mut remove_timestamps = vec![]; + let mut accumulate_amount = Uint128::zero(); + for item in bucket.range(None, None, Order::Ascending) { + let (time, amount) = item?; + let seconds = u64::from_be_bytes( + <[u8; 8]>::try_from(time.clone()) + .map_err(|_| StdError::generic_err("Casting vec to be_bytes fail"))?, + ); + if seconds > timestamp.seconds() { + break; } - None => Ok(Uint128::zero()), + remove_timestamps.push(time); + accumulate_amount += amount; } + + // remove timestamp + for time in remove_timestamps { + bucket.remove(&time); + } + + Ok(accumulate_amount) } diff --git a/contracts/oraiswap_staking/src/testing/staking_test.rs b/contracts/oraiswap_staking/src/testing/staking_test.rs index 2ed2d209..490ee1bd 100644 --- a/contracts/oraiswap_staking/src/testing/staking_test.rs +++ b/contracts/oraiswap_staking/src/testing/staking_test.rs @@ -583,6 +583,9 @@ fn test_unbonding_period_happy_case() { QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), staking_token: Addr::unchecked("staking"), + start_after: None, + limit: None, + order: None, }, ) .unwrap(); @@ -617,6 +620,9 @@ fn test_unbonding_period_happy_case() { QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), staking_token: Addr::unchecked("staking"), + start_after: None, + limit: None, + order: None, }, ) .unwrap(); @@ -684,6 +690,9 @@ fn test_unbonding_period_happy_case() { QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), staking_token: Addr::unchecked("staking"), + start_after: None, + limit: None, + order: None, }, ) .unwrap(); @@ -715,7 +724,7 @@ fn test_unbonding_period_happy_case() { } #[test] -pub fn test_max_exceed_number_lock() { +pub fn test_multiple_lock() { let unbonding_period = 10000; let mut deps = _setup_staking(Some(unbonding_period)); let info = mock_info("addr", &[]); @@ -739,36 +748,27 @@ pub fn test_max_exceed_number_lock() { QueryMsg::LockInfos { staker_addr: Addr::unchecked("addr"), staking_token: Addr::unchecked("staking"), + start_after: None, + limit: Some(30), + order: None, }, ) .unwrap(); let lock_infos = from_binary::(&binary_response).unwrap(); assert_eq!(lock_infos.lock_infos.len(), MAX_LIMIT as usize); - let msg = ExecuteMsg::Unbond { - staking_token: Addr::unchecked("staking"), - amount: Uint128::from(1u128), - }; - let mut clone_unbonded = unbond_env.clone(); - clone_unbonded.block.time = clone_unbonded - .block - .time - .plus_seconds(((MAX_LIMIT + 1) as u64) * unbonding_period / 50); - let res = execute(deps.as_mut(), clone_unbonded, info.clone(), msg).unwrap_err(); - if let StdError::GenericErr { msg, .. } = res { - assert_eq!(msg, "Exceed maximum limit of lock info"); - } else { - panic!("Must return generic error"); - } + // Since we anchor the timestamp by unbond_env, so we must add the unbonding_period to the + // block_time to get the first unlock timestamp. Then, we plus another unbonding_period to get to the rest + // of lock + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period); + unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period); - // skip to time have unlock all the lock_info - unbond_env.block.time = unbond_env.block.time.plus_seconds(unbonding_period + 1); let msg = ExecuteMsg::Unbond { staking_token: Addr::unchecked("staking"), amount: Uint128::from(0u128), }; - let res = execute(deps.as_mut(), unbond_env, info.clone(), msg).unwrap(); + let res = execute(deps.as_mut(), unbond_env.clone(), info.clone(), msg).unwrap(); assert_eq!( res.attributes, @@ -791,7 +791,23 @@ pub fn test_max_exceed_number_lock() { .unwrap(), funds: vec![], }),] + ); + + // assert after we withdraw all_lock + let binary_response = query( + deps.as_ref(), + unbond_env.clone(), + QueryMsg::LockInfos { + staker_addr: Addr::unchecked("addr"), + staking_token: Addr::unchecked("staking"), + start_after: None, + limit: None, + order: None, + }, ) + .unwrap(); + let lock_infos = from_binary::(&binary_response).unwrap(); + assert_eq!(lock_infos.lock_infos.len(), 0); } fn _setup_staking(unbonding_period: Option) -> OwnedDeps { diff --git a/packages/oraiswap/src/staking.rs b/packages/oraiswap/src/staking.rs index b0bb7494..f0611d7a 100644 --- a/packages/oraiswap/src/staking.rs +++ b/packages/oraiswap/src/staking.rs @@ -126,6 +126,10 @@ pub enum QueryMsg { LockInfos { staker_addr: Addr, staking_token: Addr, + start_after: Option, + limit: Option, + // so can convert or throw error + order: Option, }, }