From cd636ca6a9f33060d1ccb68a116e2ed36394f3e2 Mon Sep 17 00:00:00 2001 From: javiersuweijie Date: Wed, 10 Jan 2024 12:52:42 +0800 Subject: [PATCH] feat: update emissions distribution msg --- contracts/alliance-lp-hub/src/contract.rs | 138 +++-- contracts/alliance-lp-hub/src/models.rs | 27 +- contracts/alliance-lp-hub/src/query.rs | 28 +- contracts/alliance-lp-hub/src/state.rs | 4 +- contracts/alliance-lp-hub/src/tests/mod.rs | 2 +- .../alliance-lp-hub/src/tests/rewards.rs | 505 ++++++++++++++++++ .../src/tests/stake_unstake.rs | 4 - packages/alliance-protocol/src/error.rs | 3 + 8 files changed, 644 insertions(+), 67 deletions(-) create mode 100644 contracts/alliance-lp-hub/src/tests/rewards.rs diff --git a/contracts/alliance-lp-hub/src/contract.rs b/contracts/alliance-lp-hub/src/contract.rs index d4af77b..cb2c89c 100644 --- a/contracts/alliance-lp-hub/src/contract.rs +++ b/contracts/alliance-lp-hub/src/contract.rs @@ -7,27 +7,29 @@ use alliance_protocol::{ }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_json_binary, Addr, Binary, Coin as CwCoin, CosmosMsg, Decimal, DepsMut, Empty, Env, - MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, -}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Coin as CwCoin, CosmosMsg, Decimal, DepsMut, Empty, Env, MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, Order}; use cw2::set_contract_version; -use cw_asset::{Asset, AssetInfo, AssetInfoKey}; +use cw_asset::{Asset, AssetInfo, AssetInfoKey, AssetInfoUnchecked}; use cw_utils::parse_instantiate_response_data; use std::collections::HashSet; +use std::convert::TryFrom; +use std::str::FromStr; use terra_proto_rs::{ alliance::alliance::{MsgClaimDelegationRewards, MsgDelegate, MsgRedelegate, MsgUndelegate}, cosmos::base::v1beta1::Coin, traits::Message, }; +use alliance_protocol::alliance_oracle_types::EmissionsDistribution; +use alliance_protocol::alliance_protocol::AssetDistribution; use crate::{ models::{Config, ExecuteMsg, InstantiateMsg, ModifyAsset}, state::{ - ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, CONFIG, TEMP_BALANCE, + ASSET_REWARD_RATE, BALANCES, CONFIG, TEMP_BALANCE, TOTAL_BALANCES, UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, }, }; +use crate::state::UNALLOCATED_REWARDS; // version info for migration info const CONTRACT_NAME: &str = "crates.io:terra-alliance-lp-hub"; @@ -67,6 +69,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; VALIDATORS.save(deps.storage, &HashSet::new())?; + UNALLOCATED_REWARDS.save(deps.storage, &Uint128::zero())?; Ok(Response::new() .add_attributes(vec![("action", "instantiate")]) @@ -108,16 +111,15 @@ pub fn execute( ExecuteMsg::AllianceRedelegate(msg) => alliance_redelegate(deps, env, info, msg), ExecuteMsg::UpdateRewards {} => update_rewards(deps, env, info), - ExecuteMsg::RebalanceEmissions {} => rebalance_emissions(deps, env, info), + ExecuteMsg::RebalanceEmissions(distributions) => rebalance_emissions(deps, env, info, distributions), ExecuteMsg::UpdateRewardsCallback {} => update_reward_callback(deps, env, info), - ExecuteMsg::RebalanceEmissionsCallback {} => rebalance_emissions_callback(deps, env, info), + ExecuteMsg::RebalanceEmissionsCallback(distributions) => rebalance_emissions_callback(deps, env, info, distributions), } } // This method iterate through the list of assets to be modified, // for each asset it checks if it is being listed or delisted, -// when listed and an asset already exists, it updates the reward rate. fn modify_assets( deps: DepsMut, info: MessageInfo, @@ -131,25 +133,18 @@ fn modify_assets( if asset.delete { let asset_key = AssetInfoKey::from(asset.asset_info.clone()); WHITELIST.remove(deps.storage, asset_key.clone()); - ASSET_REWARD_RATE.update(deps.storage, asset_key, |_| -> StdResult<_> { - Ok(Decimal::zero()) - })?; attrs.extend_from_slice(&[ ("asset".to_string(), asset.asset_info.to_string()), ("to_remove".to_string(), asset.delete.to_string()), ]); } else { - let reward_rate = asset.is_valid_reward_rate()?; let asset_key = AssetInfoKey::from(asset.asset_info.clone()); - - WHITELIST.save(deps.storage, asset_key.clone(), &reward_rate)?; + WHITELIST.save(deps.storage, asset_key.clone(), &Decimal::zero())?; ASSET_REWARD_RATE.update(deps.storage, asset_key, |rate| -> StdResult<_> { Ok(rate.unwrap_or(Decimal::zero())) })?; - attrs.extend_from_slice(&[ ("asset".to_string(), asset.asset_info.to_string()), - ("to_rewards_rate".to_string(), reward_rate.to_string()), ]); } } @@ -491,9 +486,60 @@ fn update_reward_callback( if info.sender != env.contract.address { return Err(ContractError::Unauthorized {}); } - let _config = CONFIG.load(deps.storage)?; + let config = CONFIG.load(deps.storage)?; + // We only deal with alliance rewards here. Other rewards (e.g. ASTRO) needs to be dealt with separately + // This is because the reward distribution only affects alliance rewards. LP rewards are directly distributed to LP holders + // and not pooled together and shared + let reward_asset = AssetInfo::native(config.reward_denom); + let current_balance = reward_asset.query_balance(&deps.querier, env.contract.address)?; + let previous_balance = TEMP_BALANCE.load(deps.storage)?; + let rewards_collected = current_balance - previous_balance; + + let whitelist: StdResult> = WHITELIST + .range_raw(deps.storage, None, None, Order::Ascending) + .map(|r| r.map(|(a,d)| (AssetInfoKey(a), d))) + .collect(); + + let whitelist = whitelist?; + + let total_distribution = whitelist + .iter() + .fold(Decimal::zero(), |acc, (_,v ) | acc + v); + + // Move all unallocated rewards to the unallocated rewards bucket + if let Ok(unallocated_rewards) = Decimal::one().checked_sub(total_distribution) { + UNALLOCATED_REWARDS.update(deps.storage, |rewards| -> StdResult<_> { + Ok(rewards + unallocated_rewards.to_uint_floor()) + })?; + } else { + return Err(ContractError::InvalidTotalDistribution(total_distribution)); + } + + // Calculate the rewards for each asset + for (asset_key, distribution) in whitelist { + let total_reward_distributed: Decimal = Decimal::from_atomics(rewards_collected, 0)? + * distribution; + + // If there are no balances, we stop updating the rate. This means that the emissions are not directed to any stakers. + let total_balance = TOTAL_BALANCES + .load(deps.storage, asset_key.clone()) + .unwrap_or(Uint128::zero()); + if total_balance.is_zero() { + continue + } - // TODO: maths + // Update reward rates for each asset + let rate_to_update = + total_reward_distributed / Decimal::from_atomics(total_balance, 0)?; + if rate_to_update > Decimal::zero() { + ASSET_REWARD_RATE.update( + deps.storage, + asset_key.clone(), + |rate| -> StdResult<_> { Ok(rate.unwrap_or(Decimal::zero()) + rate_to_update) }, + )?; + } + } + TEMP_BALANCE.remove(deps.storage); Ok(Response::new().add_attributes(vec![("action", "update_rewards_callback")])) } @@ -502,37 +548,67 @@ fn rebalance_emissions( deps: DepsMut, env: Env, info: MessageInfo, + weights: Vec, ) -> Result { // Allow execution only from the controller account let config = CONFIG.load(deps.storage)?; is_controller(&info, &config)?; - // Before starting with the rebalance emission process - // rewards must be updated to the current block height - // Skip if no reward distribution in the first place - let res = if ASSET_REWARD_DISTRIBUTION.load(deps.storage).is_ok() { - update_rewards(deps, env.clone(), info)? - } else { - Response::new() - }; + // // Before starting with the rebalance emission process + // // rewards must be updated to the current block height + // // Skip if no reward distribution in the first place + // let res = if ASSET_REWARD_DISTRIBUTION.load(deps.storage).is_ok() { + // update_rewards(deps, env.clone(), info)? + // } else { + // Response::new() + // }; + let res = Response::new(); Ok(res.add_message(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: env.contract.address.to_string(), - msg: to_json_binary(&ExecuteMsg::RebalanceEmissionsCallback {}).unwrap(), + msg: to_json_binary(&ExecuteMsg::RebalanceEmissionsCallback(weights)).unwrap(), funds: vec![], }))) } fn rebalance_emissions_callback( - _deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, + distributions: Vec, ) -> Result { if info.sender != env.contract.address { return Err(ContractError::Unauthorized {}); } - // TODO maths - Ok(Response::new()) + + let total_distribution = distributions + .iter() + .map(|a| a.distribution.to_decimal().unwrap()) + .fold(Decimal::zero(), |acc, v| acc + v); + if total_distribution > Decimal::one() { + return Err(ContractError::InvalidTotalDistribution(total_distribution)); + } + + for distribution in distributions.iter() { + let asset_info: AssetInfo = AssetInfoUnchecked::from_str(&distribution.denom)?.check(deps.api, None)?; + let asset_key = AssetInfoKey::from(asset_info.clone()); + WHITELIST.update(deps.storage, asset_key, |current| -> Result<_, ContractError> { + if let Some(current) = current { + return Ok(current + distribution.distribution.to_decimal()?); + } else { + return Err(ContractError::AssetNotWhitelisted {}); + } + })?; + } + + let mut attrs = vec![("action".to_string(), "rebalance_emissions".to_string())]; + for distribution in distributions { + attrs.push(( + distribution.denom.to_string(), + distribution.distribution.to_string(), + )); + } + Ok(Response::new().add_attributes(attrs)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/alliance-lp-hub/src/models.rs b/contracts/alliance-lp-hub/src/models.rs index 7c1c0f2..c010083 100644 --- a/contracts/alliance-lp-hub/src/models.rs +++ b/contracts/alliance-lp-hub/src/models.rs @@ -9,6 +9,7 @@ use cosmwasm_std::{Addr, Decimal, Uint128}; use cw20::Cw20ReceiveMsg; use cw_asset::{Asset, AssetInfo}; use std::collections::{HashMap, HashSet}; +use alliance_protocol::alliance_oracle_types::EmissionsDistribution; pub type AssetDenom = String; @@ -49,43 +50,23 @@ pub enum ExecuteMsg { AllianceDelegate(AllianceDelegateMsg), AllianceUndelegate(AllianceUndelegateMsg), AllianceRedelegate(AllianceRedelegateMsg), - RebalanceEmissions {}, - RebalanceEmissionsCallback {}, + RebalanceEmissions(Vec), + RebalanceEmissionsCallback(Vec), } #[cw_serde] pub struct ModifyAsset { pub asset_info: AssetInfo, - pub rewards_rate: Option, pub delete: bool, } impl ModifyAsset { - pub fn new(asset_info: AssetInfo, rewards_rate: Option, delete: bool) -> Self { + pub fn new(asset_info: AssetInfo, delete: bool) -> Self { ModifyAsset { asset_info, - rewards_rate, delete, } } - - pub fn is_valid_reward_rate(&self) -> Result { - match self.rewards_rate { - Some(rate) => { - if rate < Decimal::zero() || rate > Decimal::one() { - return Err(ContractError::InvalidRewardRate( - rate, - self.asset_info.to_string(), - )); - } - Ok(rate) - } - None => Err(ContractError::InvalidRewardRate( - Decimal::zero(), - self.asset_info.to_string(), - )), - } - } } #[cw_serde] diff --git a/contracts/alliance-lp-hub/src/query.rs b/contracts/alliance-lp-hub/src/query.rs index dc6107b..76eed33 100644 --- a/contracts/alliance-lp-hub/src/query.rs +++ b/contracts/alliance-lp-hub/src/query.rs @@ -4,12 +4,16 @@ use crate::models::{ }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{to_json_binary, Binary, Deps, Env, Order, StdResult, Uint128}; -use cw_asset::{AssetInfo, AssetInfoKey}; +use cosmwasm_std::{to_json_binary, Binary, Deps, Env, Order, StdResult, Uint128, Decimal}; +use cw_asset::{AssetInfo, AssetInfoKey, AssetInfoUnchecked}; use std::collections::HashMap; +use std::convert::TryFrom; +use alliance_protocol::alliance_oracle_types::EmissionsDistribution; +use alliance_protocol::alliance_protocol::AssetDistribution; +use alliance_protocol::signed_decimal::{Sign, SignedDecimal}; use crate::state::{ - ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, CONFIG, TOTAL_BALANCES, + ASSET_REWARD_RATE, BALANCES, CONFIG, TOTAL_BALANCES, UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, }; @@ -55,9 +59,21 @@ fn get_whitelisted_assets(deps: Deps) -> StdResult { } fn get_rewards_distribution(deps: Deps) -> StdResult { - let asset_rewards_distr = ASSET_REWARD_DISTRIBUTION.load(deps.storage)?; - - to_json_binary(&asset_rewards_distr) + let whitelist: StdResult> = WHITELIST + .range(deps.storage, None, None, Order::Ascending) + .collect(); + let whitelist = whitelist?; + + let reward_distribution: Vec = whitelist + .iter() + .map(|(asset_info, distribution) | + EmissionsDistribution { + denom: asset_info.check( deps.api, None).unwrap().to_string(), + distribution: SignedDecimal::from_decimal(distribution.clone(), Sign::Positive), + } + ) + .collect(); + to_json_binary(&reward_distribution) } fn get_staked_balance(deps: Deps, asset_query: AssetQuery) -> StdResult { diff --git a/contracts/alliance-lp-hub/src/state.rs b/contracts/alliance-lp-hub/src/state.rs index 9b82904..0146277 100644 --- a/contracts/alliance-lp-hub/src/state.rs +++ b/contracts/alliance-lp-hub/src/state.rs @@ -13,11 +13,11 @@ pub const TOTAL_BALANCES: Map = Map::new("total_balances" pub const VALIDATORS: Item> = Item::new("validators"); -pub const ASSET_REWARD_DISTRIBUTION: Item> = - Item::new("asset_reward_distribution"); pub const ASSET_REWARD_RATE: Map = Map::new("asset_reward_rate"); pub const USER_ASSET_REWARD_RATE: Map<(Addr, AssetInfoKey), Decimal> = Map::new("user_asset_reward_rate"); pub const UNCLAIMED_REWARDS: Map<(Addr, AssetInfoKey), Uint128> = Map::new("unclaimed_rewards"); +// Unallocated Alliance rewards that are to be returned to the fee pool for stakers +pub const UNALLOCATED_REWARDS: Item = Item::new("unallocated_rewards"); pub const TEMP_BALANCE: Item = Item::new("temp_balance"); diff --git a/contracts/alliance-lp-hub/src/tests/mod.rs b/contracts/alliance-lp-hub/src/tests/mod.rs index 8eee544..8aae736 100644 --- a/contracts/alliance-lp-hub/src/tests/mod.rs +++ b/contracts/alliance-lp-hub/src/tests/mod.rs @@ -1,4 +1,4 @@ mod helpers; mod instantiate; mod stake_unstake; - +mod rewards; diff --git a/contracts/alliance-lp-hub/src/tests/rewards.rs b/contracts/alliance-lp-hub/src/tests/rewards.rs new file mode 100644 index 0000000..eb83582 --- /dev/null +++ b/contracts/alliance-lp-hub/src/tests/rewards.rs @@ -0,0 +1,505 @@ +use crate::contract::execute; +use crate::models::{ExecuteMsg, ModifyAsset, PendingRewardsRes}; +use crate::state::{ASSET_REWARD_RATE, TEMP_BALANCE, TOTAL_BALANCES, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST}; +use crate::tests::helpers::{ + claim_rewards, query_all_rewards, query_rewards, set_alliance_asset, setup_contract, stake, + unstake, modify_asset, DENOM, +}; +use alliance_protocol::alliance_protocol::AssetDistribution; +use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; +use cosmwasm_std::{ + coin, coins, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Response, SubMsg, + Uint128, WasmMsg, +}; +use cw_asset::{AssetInfo, AssetInfoKey}; +use std::collections::{HashMap, HashSet}; +use terra_proto_rs::alliance::alliance::MsgClaimDelegationRewards; +use terra_proto_rs::traits::Message; + +#[test] +fn test_update_rewards() { + let mut deps = mock_dependencies_with_balance(&[coin(1000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + + VALIDATORS + .save( + deps.as_mut().storage, + &HashSet::from(["validator1".to_string()]), + ) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("user", &[]), + ExecuteMsg::UpdateRewards {}, + ) + .unwrap(); + assert_eq!( + res.messages, + vec![ + SubMsg::reply_on_error( + CosmosMsg::Stargate { + type_url: "/alliance.alliance.MsgClaimDelegationRewards".to_string(), + value: Binary::from( + MsgClaimDelegationRewards { + delegator_address: "cosmos2contract".to_string(), + validator_address: "validator1".to_string(), + denom: DENOM.to_string(), + } + .encode_to_vec() + ) + }, + 2 + ), + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + funds: vec![], + contract_addr: "cosmos2contract".to_string(), + msg: to_json_binary(&ExecuteMsg::UpdateRewardsCallback {}).unwrap() + })) + ] + ); + let prev_balance = TEMP_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(prev_balance, Uint128::new(1000000)); +} + +#[test] +fn test_update_rewards_with_funds_sent() { + let mut deps = mock_dependencies_with_balance(&[coin(1000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + + VALIDATORS + .save( + deps.as_mut().storage, + &HashSet::from(["validator1".to_string(), "validator2".to_string()]), + ) + .unwrap(); + + deps.querier + .update_balance("cosmos2contract", vec![coin(2000000, "uluna")]); + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("user", &[coin(1000000, "uluna")]), + ExecuteMsg::UpdateRewards {}, + ) + .unwrap(); + let prev_balance = TEMP_BALANCE.load(deps.as_ref().storage).unwrap(); + assert_eq!(res.messages.len(), 3); + assert_eq!(prev_balance, Uint128::new(1000000)); +} + +#[test] +fn update_reward_callback() { + let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + + TOTAL_BALANCES + .save( + deps.as_mut().storage, + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + &Uint128::new(1000000), + ) + .unwrap(); + TOTAL_BALANCES + .save( + deps.as_mut().storage, + AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), + &Uint128::new(100000), + ) + .unwrap(); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(10)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(60)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aMONKEY".to_string())), &Decimal::percent(30)).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + let a_whale_rate = ASSET_REWARD_RATE + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + ) + .unwrap(); + assert_eq!( + a_whale_rate, + Decimal::from_atomics(Uint128::one(), 1).unwrap() + ); + let b_whale_rate = ASSET_REWARD_RATE + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), + ) + .unwrap(); + assert_eq!( + b_whale_rate, + Decimal::from_atomics(Uint128::new(6), 0).unwrap() + ); + ASSET_REWARD_RATE + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("cMONKEY".to_string())), + ) + .unwrap_err(); + + assert_eq!( + res, + Response::new().add_attributes(vec![("action", "update_rewards_callback"),]) + ); +} + +#[test] +fn claim_user_rewards() { + let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + modify_asset( + deps.as_mut(), + Vec::from([ + ModifyAsset{ + asset_info: AssetInfo::Native("aWHALE".to_string()), + delete: false, + } + ]), + ); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(50)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(50)).unwrap(); + + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + stake(deps.as_mut(), "user2", 4000000, "aWHALE"); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + let rewards = query_rewards(deps.as_ref(), "user1", "aWHALE"); + assert_eq!( + rewards, + PendingRewardsRes { + rewards: Uint128::new(100000), + reward_asset: AssetInfo::Native("uluna".to_string()), + staked_asset: AssetInfo::Native("aWHALE".to_string()), + } + ); + + let all_rewards = query_all_rewards(deps.as_ref(), "user1"); + assert_eq!( + all_rewards, + vec![PendingRewardsRes { + rewards: Uint128::new(100000), + reward_asset: AssetInfo::Native("uluna".to_string()), + staked_asset: AssetInfo::Native("aWHALE".to_string()), + }] + ); + + let res = claim_rewards(deps.as_mut(), "user1", "aWHALE"); + assert_eq!( + res, + Response::new() + .add_attributes(vec![ + ("action", "claim_rewards"), + ("user", "user1"), + ("asset", "native:aWHALE"), + ("reward_amount", "100000"), + ]) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".to_string(), + amount: coins(100000, "uluna"), + })) + ); + + let user_reward_rate = USER_ASSET_REWARD_RATE + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + ), + ) + .unwrap(); + let asset_reward_rate = ASSET_REWARD_RATE + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + ) + .unwrap(); + assert_eq!(user_reward_rate, asset_reward_rate); + + let rewards = query_rewards(deps.as_ref(), "user1", "aWHALE"); + assert_eq!( + rewards, + PendingRewardsRes { + rewards: Uint128::new(0), + reward_asset: AssetInfo::Native("uluna".to_string()), + staked_asset: AssetInfo::Native("aWHALE".to_string()), + } + ); + + let all_rewards = query_all_rewards(deps.as_ref(), "user1"); + assert_eq!( + all_rewards, + vec![PendingRewardsRes { + rewards: Uint128::new(0), + reward_asset: AssetInfo::Native("uluna".to_string()), + staked_asset: AssetInfo::Native("aWHALE".to_string()), + }] + ); + + let res = claim_rewards(deps.as_mut(), "user1", "aWHALE"); + assert_eq!( + res, + Response::new().add_attributes(vec![ + ("action", "claim_rewards"), + ("user", "user1"), + ("asset", "native:aWHALE"), + ("reward_amount", "0"), + ]) + ); + + // Update more rewards + deps.querier + .update_balance("cosmos2contract", vec![coin(1900000 + 100000, "uluna")]); + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1900000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + let res = claim_rewards(deps.as_mut(), "user1", "aWHALE"); + assert_eq!( + res, + Response::new() + .add_attributes(vec![ + ("action", "claim_rewards"), + ("user", "user1"), + ("asset", "native:aWHALE"), + ("reward_amount", "10000"), + ]) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".to_string(), + amount: coins(10000, "uluna"), + })) + ); +} + +#[test] +fn claim_user_rewards_after_staking() { + let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + modify_asset( + deps.as_mut(), + Vec::from([ + ModifyAsset{ + asset_info: AssetInfo::Native("aWHALE".to_string()), + delete: false, + } + ]), + ); + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + stake(deps.as_mut(), "user2", 4000000, "aWHALE"); + + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(50)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(50)).unwrap(); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + + let res = claim_rewards(deps.as_mut(), "user1", "aWHALE"); + assert_eq!( + res, + Response::new() + .add_attributes(vec![ + ("action", "claim_rewards"), + ("user", "user1"), + ("asset", "native:aWHALE"), + ("reward_amount", "100000"), + ]) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".to_string(), + amount: coins(100000, "uluna"), + })) + ); + + // Claiming again should get 0 rewards + let res = claim_rewards(deps.as_mut(), "user1", "aWHALE"); + assert_eq!( + res, + Response::new().add_attributes(vec![ + ("action", "claim_rewards"), + ("user", "user1"), + ("asset", "native:aWHALE"), + ("reward_amount", "0"), + ]) + ); +} + +#[test] +fn claim_rewards_after_staking_and_unstaking() { + let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + modify_asset( + deps.as_mut(), + Vec::from([ + ModifyAsset{ + asset_info: AssetInfo::Native("aWHALE".to_string()), + delete: false, + }, + ModifyAsset{ + asset_info: AssetInfo::Native("bWHALE".to_string()), + delete: false, + } + ]), + ); + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + stake(deps.as_mut(), "user2", 4000000, "aWHALE"); + stake(deps.as_mut(), "user2", 1000000, "bWHALE"); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(50)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(50)).unwrap(); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + claim_rewards(deps.as_mut(), "user1", "aWHALE"); + + // Get asset reward rate + let prev_rate = ASSET_REWARD_RATE + .load( + deps.as_mut().storage, + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + ) + .unwrap(); + + // Unstake + unstake(deps.as_mut(), "user1", 1000000, "aWHALE"); + + // Accrue rewards again + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + let curr_rate = ASSET_REWARD_RATE + .load( + deps.as_mut().storage, + AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), + ) + .unwrap(); + assert!(curr_rate > prev_rate); + + // User 1 stakes back + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + + // User 1 should not have any rewards + let rewards = query_rewards(deps.as_ref(), "user1", "aWHALE"); + assert_eq!(rewards.rewards, Uint128::zero()); + + // User 2 should receive all the rewards in the contract + let rewards = query_rewards(deps.as_ref(), "user2", "aWHALE"); + assert_eq!(rewards.rewards, Uint128::new(900000)); + let rewards = query_rewards(deps.as_ref(), "user2", "bWHALE"); + assert_eq!(rewards.rewards, Uint128::new(1000000)); +} + +#[test] +fn claim_rewards_after_rebalancing_emissions() { + let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); + setup_contract(deps.as_mut()); + set_alliance_asset(deps.as_mut()); + modify_asset( + deps.as_mut(), + Vec::from([ + ModifyAsset{ + asset_info: AssetInfo::Native("aWHALE".to_string()), + delete: false, + }, + ModifyAsset{ + asset_info: AssetInfo::Native("bWHALE".to_string()), + delete: false, + } + ]), + ); + stake(deps.as_mut(), "user1", 1000000, "aWHALE"); + stake(deps.as_mut(), "user2", 1000000, "bWHALE"); + + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(50)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(50)).unwrap(); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("aWHALE".to_string())), &Decimal::percent(100)).unwrap(); + WHITELIST.save(deps.as_mut().storage, AssetInfoKey::from(AssetInfo::Native("bWHALE".to_string())), &Decimal::percent(0)).unwrap(); + + TEMP_BALANCE + .save(deps.as_mut().storage, &Uint128::new(1000000)) + .unwrap(); + execute( + deps.as_mut(), + mock_env(), + mock_info("cosmos2contract", &[]), + ExecuteMsg::UpdateRewardsCallback {}, + ) + .unwrap(); + + let rewards = query_rewards(deps.as_ref(), "user1", "aWHALE"); + assert_eq!(rewards.rewards, Uint128::new(1500000)); + // User 2 should receive all the rewards in the contract + let rewards = query_rewards(deps.as_ref(), "user2", "bWHALE"); + assert_eq!(rewards.rewards, Uint128::new(500000)); +} diff --git a/contracts/alliance-lp-hub/src/tests/stake_unstake.rs b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs index 4931402..7996a45 100644 --- a/contracts/alliance-lp-hub/src/tests/stake_unstake.rs +++ b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs @@ -19,7 +19,6 @@ fn test_stake() { vec![ ModifyAsset { asset_info: AssetInfo::native(Addr::unchecked("native_asset")), - rewards_rate: Some(Decimal::new(Uint128::new(500_000_000_000_000_000u128))), delete: false, } ] @@ -96,7 +95,6 @@ fn test_stake_cw20() { vec![ ModifyAsset { asset_info: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), - rewards_rate: Some(Decimal::new(Uint128::new(500_000_000_000_000_000u128))), delete: false, } ] @@ -174,7 +172,6 @@ fn test_unstake() { vec![ ModifyAsset { asset_info: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), - rewards_rate: Some(Decimal::new(Uint128::new(500_000_000_000_000_000u128))), delete: false, } ] @@ -254,7 +251,6 @@ fn test_unstake_invalid() { vec![ ModifyAsset { asset_info: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), - rewards_rate: Some(Decimal::new(Uint128::new(500_000_000_000_000_000u128))), delete: false, } ] diff --git a/packages/alliance-protocol/src/error.rs b/packages/alliance-protocol/src/error.rs index 928bc67..4086452 100644 --- a/packages/alliance-protocol/src/error.rs +++ b/packages/alliance-protocol/src/error.rs @@ -36,4 +36,7 @@ pub enum ContractError { #[error("Invalid reward rate '{0}' for denom '{1}'")] InvalidRewardRate(Decimal, String), + + #[error("Invalid total distribution: {0}")] + InvalidTotalDistribution(Decimal), }