diff --git a/contracts/alliance-lp-hub/src/contract.rs b/contracts/alliance-lp-hub/src/contract.rs index 536a8b8..13625e8 100644 --- a/contracts/alliance-lp-hub/src/contract.rs +++ b/contracts/alliance-lp-hub/src/contract.rs @@ -1,13 +1,15 @@ use crate::{ astro_models::{Cw20Msg, ExecuteAstroMsg, QueryAstroMsg, RewardInfo}, helpers::{is_controller, is_governance}, - models::{Config, ExecuteMsg, InstantiateMsg, ModifyAssetPair}, + models::{ + from_string_to_asset_info, AstroClaimRewardsPosition, Config, ExecuteMsg, InstantiateMsg, + ModifyAssetPair, + }, state::{ ASSET_REWARD_RATE, BALANCES, CONFIG, TEMP_BALANCE, TOTAL_BALANCES, UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, }, }; -use alliance_protocol::alliance_oracle_types::EmissionsDistribution; use alliance_protocol::{ alliance_protocol::{ AllianceDelegateMsg, AllianceRedelegateMsg, AllianceUndelegateMsg, MigrateMsg, @@ -25,7 +27,7 @@ use cw2::set_contract_version; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_asset::{Asset, AssetInfo, AssetInfoKey}; use cw_utils::parse_instantiate_response_data; -use std::{collections::HashSet, env}; +use std::{collections::HashSet, env, str::FromStr}; use terra_proto_rs::{ alliance::alliance::{MsgClaimDelegationRewards, MsgDelegate, MsgRedelegate, MsgUndelegate}, cosmos::base::v1beta1::Coin, @@ -37,7 +39,7 @@ const CONTRACT_NAME: &str = "crates.io:terra-alliance-lp-hub"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const CREATE_REPLY_ID: u64 = 1; const CLAIM_ALLIANCE_REWARDS_ERROR_REPLY_ID: u64 = 2; -const CLAIM_ASTRO_REWARDS_ERROR_REPLY_ID: u64 = 3; +const CLAIM_ASTRO_REWARDS_REPLY_ID: u64 = 3; #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { @@ -68,11 +70,11 @@ pub fn instantiate( let config = Config { governance: governance_address, controller: controller_address, - fee_collector: fee_collector_address, - astro_incentives: astro_incentives_address, + fee_collector_addr: fee_collector_address, + astro_incentives_addr: astro_incentives_address, alliance_token_denom: "".to_string(), alliance_token_supply: Uint128::zero(), - reward_denom: msg.reward_denom, + alliance_reward_denom: msg.reward_denom, }; CONFIG.save(deps.storage, &config)?; @@ -124,7 +126,6 @@ pub fn execute( ExecuteMsg::UpdateAllianceRewardsCallback {} => { update_alliance_reward_callback(deps, env, info) } - ExecuteMsg::UpdateAstroRewardsCallback {} => update_astro_reward_callback(deps, env, info), } } @@ -187,7 +188,7 @@ fn stake( .load(deps.storage, asset_key.clone()) .map_err(|_| ContractError::AssetNotWhitelisted(received_asset.info.to_string()))?; let config = CONFIG.load(deps.storage)?; - let reward_token = AssetInfoKey::from(AssetInfo::Native(config.alliance_token_denom)); + let reward_token = AssetInfoKey::from(AssetInfo::Native(config.alliance_reward_denom)); let rewards = _claim_alliance_rewards( deps.storage, sender.clone(), @@ -199,7 +200,7 @@ fn stake( deps.storage, (sender.clone(), asset_key.clone()), |balance| -> Result<_, ContractError> { - let mut unclaimed_rewards = balance.unwrap_or(Uint128::zero()); + let mut unclaimed_rewards = balance.unwrap_or_default(); unclaimed_rewards += rewards; Ok(unclaimed_rewards) }, @@ -212,7 +213,7 @@ fn stake( let astro_incentives: Vec = deps .querier .query_wasm_smart( - config.astro_incentives.to_string(), + config.astro_incentives_addr.to_string(), &QueryAstroMsg::RewardInfo { lp_token: lp_token.split(':').collect::>()[1].to_string(), }, @@ -232,7 +233,7 @@ fn stake( // If the asset is native, we need to send it to the astro incentives contract // using the ExecuteAstroMsg::Deposit message CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.astro_incentives.to_string(), + contract_addr: config.astro_incentives_addr.to_string(), msg: to_json_binary(&ExecuteAstroMsg::Deposit { recipient: None })?, funds: vec![CwCoin { denom: native_asset, @@ -246,7 +247,7 @@ fn stake( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cw20_contract_addr.to_string(), msg: to_json_binary(&Cw20ExecuteMsg::Send { - contract: config.astro_incentives.to_string(), + contract: config.astro_incentives_addr.to_string(), amount: received_asset.amount, msg: to_json_binary(&Cw20ReceiveMsg { sender: env.contract.address.to_string(), @@ -284,17 +285,6 @@ fn stake( Ok(balance.unwrap_or(Uint128::zero()) + received_asset.amount) }, )?; - - let asset_reward_rate = ASSET_REWARD_RATE - .load(deps.storage, asset_key.clone()) - .unwrap_or_default(); - - USER_ASSET_REWARD_RATE.save( - deps.storage, - (sender.clone(), asset_key), - &asset_reward_rate, - )?; - Ok(res) } @@ -391,7 +381,7 @@ fn claim_rewards( ]); if !final_rewards.is_zero() { let rewards_asset = Asset { - info: AssetInfo::Native(config.reward_denom), + info: AssetInfo::Native(config.alliance_reward_denom), amount: final_rewards, }; Ok(response.add_message(rewards_asset.transfer_msg(&user)?)) @@ -407,29 +397,27 @@ fn _claim_alliance_rewards( reward_denom: AssetInfoKey, ) -> Result { let asset_key = AssetInfoKey::from(&staked_asset); - let asset_reward_rate = ASSET_REWARD_RATE.load(storage, (asset_key.clone(), reward_denom))?; - let user_staked = BALANCES.load(storage, (user.clone(), asset_key.clone()))?; - let user_staked = Decimal::from_atomics(user_staked, 0)?; + let state_key = (user.clone(), asset_key.clone(), reward_denom.clone()); + let user_reward_rate = USER_ASSET_REWARD_RATE.load(storage, state_key.clone()); + let asset_reward_rate = ASSET_REWARD_RATE + .load(storage, (asset_key.clone(), reward_denom.clone())) + .unwrap_or_default(); - let user_reward_rate = USER_ASSET_REWARD_RATE.load(storage, (user, asset_key, reward_denom)); if let Ok(user_reward_rate) = user_reward_rate { + let user_staked = BALANCES + .load(storage, (user.clone(), asset_key.clone())) + .unwrap_or_default(); + let user_staked = Decimal::from_atomics(user_staked, 0)?; let rewards = ((asset_reward_rate - user_reward_rate) * user_staked).to_uint_floor(); + if rewards.is_zero() { return Ok(Uint128::zero()); } else { - USER_ASSET_REWARD_RATE.save( - storage, - (user, asset_key, reward_denom), - &asset_reward_rate, - )?; + USER_ASSET_REWARD_RATE.save(storage, state_key, &asset_reward_rate)?; return Ok(rewards); } } else { - USER_ASSET_REWARD_RATE.save( - storage, - (user, asset_key, reward_denom), - &asset_reward_rate, - )?; + USER_ASSET_REWARD_RATE.save(storage, state_key, &asset_reward_rate)?; return Ok(Uint128::zero()); } } @@ -538,16 +526,6 @@ fn alliance_redelegate( .add_messages(msgs)) } -// Example of a response https://terrasco.pe/testnet/tx/EC20D82F519B8B76EBFF1DDB75592330CA5A1CACE21943B22BAA4F46468AB5E7 -fn update_astro_reward_callback( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - // TODO: add astro rewards to the state of the app - Ok(Response::new().add_attributes(vec![("action", "reply_astro_rewards")])) -} - fn _update_astro_rewards( deps: &DepsMut, contract_addr: Addr, @@ -588,9 +566,9 @@ fn _update_astro_rewards( funds: vec![], }); - return Ok(Some(SubMsg::reply_on_error( + return Ok(Some(SubMsg::reply_always( msg, - CLAIM_ASTRO_REWARDS_ERROR_REPLY_ID, + CLAIM_ASTRO_REWARDS_REPLY_ID, ))); } @@ -600,34 +578,43 @@ fn _update_astro_rewards( fn update_rewards(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let config = CONFIG.load(deps.storage)?; let mut res = Response::new().add_attributes(vec![("action", "update_rewards")]); - let reward_sent_in_tx: Option<&CwCoin> = - info.funds.iter().find(|c| c.denom == config.reward_denom); - let sent_balance = if let Some(coin) = reward_sent_in_tx { - coin.amount + + // Iterate over received funds, check the balance of the smart contract + // and finally update the temp balance with the difference between the + // contract balance and coin received. + if !info.funds.is_empty() { + info.funds.iter().try_for_each(|coin| { + let asset_info = AssetInfo::Native(coin.denom.clone()); + let contract_balance = + asset_info.query_balance(&deps.querier, env.contract.address.clone())?; + + TEMP_BALANCE.save( + deps.storage, + AssetInfoKey::from(asset_info), + &(contract_balance - coin.amount), + ) + })?; } else { - Uint128::zero() - }; + let asset_info = AssetInfo::Native(config.alliance_reward_denom.clone()); + let contract_balance = + asset_info.query_balance(&deps.querier, env.contract.address.clone())?; + + TEMP_BALANCE.save( + deps.storage, + AssetInfoKey::from(asset_info), + &contract_balance, + )? + } let astr_incentives_rewards = _update_astro_rewards( &deps, env.contract.address.clone(), - config.astro_incentives.clone(), + config.astro_incentives_addr.clone(), )?; if let Some(msg) = astr_incentives_rewards { res = res.add_submessage(msg) }; - let reward_asset = AssetInfo::native(config.reward_denom.clone()); - let contract_balance = - reward_asset.query_balance(&deps.querier, env.contract.address.clone())?; - - // Contract balance is guaranteed to be greater than sent balance - // since contract balance = previous contract balance + sent balance > sent balance - TEMP_BALANCE.save( - deps.storage, - AssetInfoKey::from(AssetInfo::Native(config.reward_denom.to_string())), - &(contract_balance - sent_balance), - )?; let validators = VALIDATORS.load(deps.storage)?; let alliance_sub_msg: Vec = validators .iter() @@ -663,13 +650,14 @@ fn update_alliance_reward_callback( return Err(ContractError::Unauthorized {}); } let config = CONFIG.load(deps.storage)?; - let mut res = Response::new().add_attributes(vec![("action", "update_rewards_callback")]); + let mut res = + Response::new().add_attributes(vec![("action", "update_alliance_rewards_callback")]); // 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.clone()); + let reward_asset = AssetInfo::native(config.alliance_reward_denom.clone()); let reward_asset_info_key = - AssetInfoKey::from(AssetInfo::Native(config.reward_denom.to_string())); + AssetInfoKey::from(AssetInfo::Native(config.alliance_reward_denom.to_string())); let current_balance = reward_asset.query_balance(&deps.querier, env.contract.address)?; let previous_balance = TEMP_BALANCE.load(deps.storage, reward_asset_info_key.clone())?; let rewards_collected = current_balance - previous_balance; @@ -691,10 +679,10 @@ fn update_alliance_reward_callback( .to_uint_floor(); if !unallocated_rewards.is_zero() { res = res.add_message(BankMsg::Send { - to_address: config.fee_collector.to_string(), + to_address: config.fee_collector_addr.to_string(), amount: vec![CwCoin::new( unallocated_rewards.u128(), - config.reward_denom.clone(), + config.alliance_reward_denom.clone(), )], }) } @@ -720,7 +708,7 @@ fn update_alliance_reward_callback( if rate_to_update > Decimal::zero() { ASSET_REWARD_RATE.update( deps.storage, - (asset_key.clone(), reward_asset_info_key), + (asset_key.clone(), reward_asset_info_key.clone()), |rate| -> StdResult<_> { let mut reward_rate = rate.unwrap_or_default(); reward_rate = reward_rate + rate_to_update; @@ -731,7 +719,7 @@ fn update_alliance_reward_callback( } TEMP_BALANCE.remove( deps.storage, - AssetInfoKey::from(AssetInfo::Native(config.reward_denom.to_string())), + AssetInfoKey::from(AssetInfo::Native(config.alliance_reward_denom.to_string())), ); Ok(res) @@ -744,58 +732,151 @@ pub fn reply( reply: Reply, ) -> Result, ContractError> { match reply.id { - CREATE_REPLY_ID => { - let response = reply.result.unwrap(); - // It works because the response data is a protobuf encoded string that contains the denom in the first slot (similar to the contract instantiation response) - let denom = parse_instantiate_response_data(response.data.unwrap().as_slice()) - .map_err(|_| ContractError::Std(StdError::generic_err("parse error".to_string())))? - .contract_address; - let total_supply = Uint128::from(1_000_000_000_000_u128); - let sub_msg_mint = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( - TokenExecuteMsg::MintTokens { - denom: denom.clone(), - amount: total_supply, - mint_to_address: env.contract.address.to_string(), - }, - ))); - CONFIG.update(deps.storage, |mut config| -> Result<_, ContractError> { - config.alliance_token_denom = denom.clone(); - config.alliance_token_supply = total_supply; - Ok(config) - })?; - - let sub_msg_metadata = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( - TokenExecuteMsg::SetMetadata { - denom: denom.clone(), - metadata: Metadata { - description: "Staking token for alliance protocol lp hub contract" - .to_string(), - denom_units: vec![DenomUnit { - denom: denom.clone(), - exponent: 0, - aliases: vec![], - }], - base: denom.to_string(), - display: denom.to_string(), - name: "Alliance LP Token".to_string(), - symbol: "ALLIANCE_LP".to_string(), - }, - }, - ))); - Ok(Response::new() - .add_attributes(vec![ - ("alliance_token_denom", denom), - ("alliance_token_total_supply", total_supply.to_string()), - ]) - .add_submessage(sub_msg_mint) - .add_submessage(sub_msg_metadata)) - } + CREATE_REPLY_ID => reply_instantiate(deps, env, reply), CLAIM_ALLIANCE_REWARDS_ERROR_REPLY_ID => { Ok(Response::new().add_attributes(vec![("action", "claim_alliance_rewards_error")])) } - CLAIM_ASTRO_REWARDS_ERROR_REPLY_ID => { - Ok(Response::new().add_attributes(vec![("action", "claim_astro_rewards_error")])) - } + CLAIM_ASTRO_REWARDS_REPLY_ID => reply_claim_astro_rewards(deps, reply), _ => Err(ContractError::InvalidReplyId(reply.id)), } } + +fn reply_instantiate( + deps: DepsMut, + env: Env, + reply: Reply, +) -> Result, ContractError> { + let response = reply.result.unwrap(); + // It works because the response data is a protobuf encoded string that contains the denom in the first slot (similar to the contract instantiation response) + let denom = parse_instantiate_response_data(response.data.unwrap().as_slice()) + .map_err(|_| ContractError::Std(StdError::generic_err("parse error".to_string())))? + .contract_address; + let total_supply = Uint128::from(1_000_000_000_000_u128); + let sub_msg_mint = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::MintTokens { + denom: denom.clone(), + amount: total_supply, + mint_to_address: env.contract.address.to_string(), + }, + ))); + CONFIG.update(deps.storage, |mut config| -> Result<_, ContractError> { + config.alliance_token_denom = denom.clone(); + config.alliance_token_supply = total_supply; + Ok(config) + })?; + + let sub_msg_metadata = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::SetMetadata { + denom: denom.clone(), + metadata: Metadata { + description: "Staking token for alliance protocol lp hub contract".to_string(), + denom_units: vec![DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec![], + }], + base: denom.to_string(), + display: denom.to_string(), + name: "Alliance LP Token".to_string(), + symbol: "ALLIANCE_LP".to_string(), + }, + }, + ))); + Ok(Response::new() + .add_attributes(vec![ + ("alliance_token_denom", denom), + ("alliance_token_total_supply", total_supply.to_string()), + ]) + .add_submessage(sub_msg_mint) + .add_submessage(sub_msg_metadata)) +} + +// This is an example of a response from claiming astro rewards +// https://terrasco.pe/testnet/tx/EC20D82F519B8B76EBFF1DDB75592330CA5A1CACE21943B22BAA4F46468AB5E7 +fn reply_claim_astro_rewards( + deps: DepsMut, + reply: Reply, +) -> Result, ContractError> { + let mut res = Response::new(); + + // Check if the reply result returns an error and + // if it does so wrap the error in the response + // attribute and return the response. + if reply.result.is_err() { + res = res.add_attributes(vec![ + ("action", "claim_astro_rewards_error"), + ("error", &reply.result.unwrap_err()), + ]); + + return Ok(res); + } + + // If the request has been successful, we need to check + // if the contract address and the event attributes are + // correct and account for the rewards. + let config = CONFIG.load(deps.storage)?; + let result = reply.result.unwrap(); + let event = result + .events + .iter() + .find(|event| event.ty == "wasm") + .ok_or_else(|| StdError::generic_err("cannot find `wasm` event on astro reply"))?; + + // Check if the callback comes from the correct contract + let first_attr = event.attributes[0].clone(); + let event_key = first_attr.key.clone(); + let even_value = first_attr.value.clone(); + if event_key != "_contract_address" && even_value != config.astro_incentives_addr { + return Err(ContractError::InvalidContractCallback( + event_key, even_value, + )); + } + + // In the list of attributes we need to find the **claimed_positions**, + // and group all the **claimed_reward** for each claimed position. + let mut astro_claims: Vec = vec![]; + let mut astro_claim = AstroClaimRewardsPosition::default(); + let mut add_to_next_claim_position = false; + for attr in event.attributes.iter() { + if attr.key == "claimed_position" { + if add_to_next_claim_position { + astro_claims.push(astro_claim.clone()); + astro_claim = AstroClaimRewardsPosition::default(); + add_to_next_claim_position = false; + } else { + astro_claim.deposited_asset = attr.value.clone(); + } + } + + if attr.key == "claimed_reward" { + let coin = CwCoin::from_str(&attr.value)?; + astro_claim.rewards.add(coin)?; + add_to_next_claim_position = true + } + } + + // Given the claimed rewards account them to the state of the contract + // dividing the reward_amount per total_lp_staked summing the already + // accounted asset_reward_rate. + for claim in astro_claims { + let deposit_asset_key = from_string_to_asset_info(claim.deposited_asset)?; + let total_lp_staked = TOTAL_BALANCES.load(deps.storage, deposit_asset_key.clone())?; + let total_lp_staked = Decimal::from_atomics(total_lp_staked, 0)?; + + for reward in claim.rewards { + let reward_asset_key = from_string_to_asset_info(reward.denom)?; + let reward_ammount = Decimal::from_atomics(reward.amount, 0)?; + + ASSET_REWARD_RATE.update( + deps.storage, + (deposit_asset_key.clone(), reward_asset_key), + |a| -> StdResult<_> { + let mut asset_reward_rate = a.unwrap_or_default(); + asset_reward_rate = (reward_ammount / total_lp_staked) + asset_reward_rate; + Ok(asset_reward_rate) + }, + )?; + } + } + return Ok(res.add_attribute("action", "claim_astro_rewards_success")); +} diff --git a/contracts/alliance-lp-hub/src/models.rs b/contracts/alliance-lp-hub/src/models.rs index 6c5059d..3c8f4de 100644 --- a/contracts/alliance-lp-hub/src/models.rs +++ b/contracts/alliance-lp-hub/src/models.rs @@ -1,11 +1,16 @@ -use alliance_protocol::alliance_protocol::{ +use alliance_protocol::{ + alliance_protocol::{ AllianceDelegateMsg, AllianceRedelegateMsg, AllianceUndelegateMsg, AssetDistribution, + }, + error::ContractError, }; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, Coins, Uint128}; use cw20::Cw20ReceiveMsg; -use cw_asset::{Asset, AssetInfo}; -use std::collections::{HashMap, HashSet}; +use cw_asset::{Asset, AssetInfo, AssetInfoKey}; +use std::{ + collections::{HashMap, HashSet}, +}; pub type AssetDenom = String; @@ -13,11 +18,11 @@ pub type AssetDenom = String; pub struct Config { pub governance: Addr, pub controller: Addr, - pub fee_collector: Addr, - pub astro_incentives: Addr, + pub fee_collector_addr: Addr, + pub astro_incentives_addr: Addr, pub alliance_token_denom: String, pub alliance_token_supply: Uint128, - pub reward_denom: String, + pub alliance_reward_denom: String, } #[cw_serde] @@ -31,7 +36,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { - // Privileged function used to whitelist, + // Privileged function used to whitelist, // modify or delete assets from the allowed list ModifyAssetPairs(Vec), @@ -44,7 +49,7 @@ pub enum ExecuteMsg { Unstake(Asset), ClaimRewards(AssetInfo), - // Alliance interactions used to delegate, undelegate and redelegate + // Alliance interactions used to delegate, undelegate and redelegate AllianceDelegate(AllianceDelegateMsg), AllianceUndelegate(AllianceUndelegateMsg), AllianceRedelegate(AllianceRedelegateMsg), @@ -52,7 +57,6 @@ pub enum ExecuteMsg { // Rewards related messages UpdateRewards {}, UpdateAllianceRewardsCallback {}, - UpdateAstroRewardsCallback {}, } #[cw_serde] @@ -132,4 +136,23 @@ pub struct AssetQuery { pub struct StakedBalanceRes { pub deposit_asset: AssetInfo, pub balance: Uint128, -} \ No newline at end of file +} + +#[derive(Clone, Default)] +pub struct AstroClaimRewardsPosition { + pub deposited_asset: String, + pub rewards: Coins, +} + +pub fn from_string_to_asset_info(denom: String) -> Result { + if denom.starts_with("ibc/") || denom.starts_with("factory/") { + let asset_info = AssetInfoKey::from(AssetInfo::Native(denom)); + return Ok(asset_info); + } else if denom.starts_with("terra") { + let from = Addr::unchecked(denom); + let asset_info = AssetInfoKey::from(AssetInfo::Cw20(from)); + return Ok(asset_info); + } + + Err(ContractError::InvalidDenom(denom)) +} diff --git a/contracts/alliance-lp-hub/src/query.rs b/contracts/alliance-lp-hub/src/query.rs index 6aa38d8..451d64c 100644 --- a/contracts/alliance-lp-hub/src/query.rs +++ b/contracts/alliance-lp-hub/src/query.rs @@ -1,6 +1,6 @@ use crate::models::{ - AllPendingRewardsQuery, AllStakedBalancesQuery, AssetQuery, - PendingRewardsRes, QueryMsg, StakedBalanceRes, WhitelistedAssetsResponse, + AllPendingRewardsQuery, AllStakedBalancesQuery, AssetQuery, PendingRewardsRes, QueryMsg, + StakedBalanceRes, WhitelistedAssetsResponse, }; use alliance_protocol::alliance_oracle_types::EmissionsDistribution; use alliance_protocol::signed_decimal::{Sign, SignedDecimal}; @@ -84,26 +84,26 @@ fn get_staked_balance(deps: Deps, asset_query: AssetQuery) -> StdResult } fn get_pending_rewards(deps: Deps, asset_query: AssetQuery) -> StdResult { - let config = CONFIG.load(deps.storage)?; let addr = deps.api.addr_validate(&asset_query.address)?; let reward_asset = AssetInfoKey::from(asset_query.reward_asset.clone()); let deposit_asset = AssetInfoKey::from(asset_query.deposit_asset.clone()); - let key = (addr, deposit_asset, reward_asset); + let key = (addr.clone(), deposit_asset.clone(), reward_asset.clone()); let user_reward_rate = USER_ASSET_REWARD_RATE.load(deps.storage, key)?; - let asset_reward_rate = ASSET_REWARD_RATE.load(deps.storage, (deposit_asset, reward_asset))?; - let user_balance = BALANCES.load(deps.storage, (addr, deposit_asset).clone())?; + let asset_reward_rate = + ASSET_REWARD_RATE.load(deps.storage, (deposit_asset.clone(), reward_asset.clone()))?; + let user_balance = BALANCES.load(deps.storage, (addr.clone(), deposit_asset).clone())?; let unclaimed_rewards = UNCLAIMED_REWARDS .load(deps.storage, (addr, reward_asset)) .unwrap_or_default(); - let alliance_pending_rewards = (asset_reward_rate - user_reward_rate) * user_balance; + let pending_rewards = (asset_reward_rate - user_reward_rate) * user_balance; to_json_binary(&PendingRewardsRes { deposit_asset: asset_query.deposit_asset, reward_asset: asset_query.reward_asset, - rewards: alliance_pending_rewards + unclaimed_rewards, + rewards: pending_rewards + unclaimed_rewards, }) } @@ -145,8 +145,8 @@ fn get_all_pending_rewards(deps: Deps, query: AllPendingRewardsQuery) -> StdResu let reward_asset = AssetInfoKey::from(assets.1.check(deps.api, None)?); let asset_reward_rate = - ASSET_REWARD_RATE.load(deps.storage, (deposit_asset, reward_asset))?; - let user_balance = BALANCES.load(deps.storage, (addr.clone(), deposit_asset))?; + ASSET_REWARD_RATE.load(deps.storage, (deposit_asset.clone(), reward_asset.clone()))?; + let user_balance = BALANCES.load(deps.storage, (addr.clone(), deposit_asset.clone()))?; let unclaimed_rewards = UNCLAIMED_REWARDS .load(deps.storage, (addr.clone(), deposit_asset)) .unwrap_or_default(); @@ -155,7 +155,7 @@ fn get_all_pending_rewards(deps: Deps, query: AllPendingRewardsQuery) -> StdResu Ok(PendingRewardsRes { rewards: alliance_pending_rewards + unclaimed_rewards, deposit_asset: assets.0.check(deps.api, None)?, - reward_asset: AssetInfo::Native(config.reward_denom.to_string()), + reward_asset: AssetInfo::Native(config.alliance_reward_denom.to_string()), }) }) .collect::>>(); diff --git a/contracts/alliance-lp-hub/src/state.rs b/contracts/alliance-lp-hub/src/state.rs index 2eb032a..5540a09 100644 --- a/contracts/alliance-lp-hub/src/state.rs +++ b/contracts/alliance-lp-hub/src/state.rs @@ -33,3 +33,8 @@ pub const USER_ASSET_REWARD_RATE: Map<(Addr, AssetInfoKey, AssetInfoKey), Decima pub const UNCLAIMED_REWARDS: Map<(Addr, AssetInfoKey), Uint128> = Map::new("unclaimed_rewards"); pub const TEMP_BALANCE: Map = Map::new("temp_balance"); + +// Temporary variable used to store the user address +// so we can access it on reply_claim_astro_rewards +// callback function and account for the rewards +pub const TEMP_USR_ADDR: Item = Item::new("temp_addr_stake"); \ No newline at end of file diff --git a/contracts/alliance-lp-hub/src/tests/instantiate.rs b/contracts/alliance-lp-hub/src/tests/instantiate.rs index fd69241..e6a83ea 100644 --- a/contracts/alliance-lp-hub/src/tests/instantiate.rs +++ b/contracts/alliance-lp-hub/src/tests/instantiate.rs @@ -37,9 +37,9 @@ fn test_setup_contract() { Config { governance: Addr::unchecked("gov"), controller: Addr::unchecked("controller"), - fee_collector: Addr::unchecked("collector_address"), - astro_incentives: Addr::unchecked("astro_incentives"), - reward_denom: "uluna".to_string(), + fee_collector_addr: Addr::unchecked("collector_address"), + astro_incentives_addr: Addr::unchecked("astro_incentives"), + alliance_reward_denom: "uluna".to_string(), alliance_token_denom: "".to_string(), alliance_token_supply: Uint128::new(0), } @@ -109,9 +109,9 @@ fn test_reply_create_token() { Config { governance: Addr::unchecked("gov"), controller: Addr::unchecked("controller"), - fee_collector: Addr::unchecked("collector_address"), - astro_incentives: Addr::unchecked("astro_incentives"), - reward_denom: "uluna".to_string(), + fee_collector_addr: Addr::unchecked("collector_address"), + astro_incentives_addr: Addr::unchecked("astro_incentives"), + alliance_reward_denom: "uluna".to_string(), alliance_token_denom: "factory/cosmos2contract/ualliancelp".to_string(), alliance_token_supply: Uint128::new(1000000000000), } diff --git a/contracts/alliance-lp-hub/src/tests/rewards.rs b/contracts/alliance-lp-hub/src/tests/rewards.rs index 3453e37..9d08ef3 100644 --- a/contracts/alliance-lp-hub/src/tests/rewards.rs +++ b/contracts/alliance-lp-hub/src/tests/rewards.rs @@ -103,7 +103,7 @@ fn test_update_rewards_with_funds_sent() { } #[test] -fn update_reward_callback() { +fn update_alliance_reward_callback() { let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); setup_contract(deps.as_mut()); set_alliance_asset(deps.as_mut()); @@ -198,12 +198,12 @@ fn update_reward_callback() { assert_eq!( res, - Response::new().add_attributes(vec![("action", "update_rewards_callback"),]) + Response::new().add_attributes(vec![("action", "update_alliance_rewards_callback"),]) ); } #[test] -fn update_reward_callback_with_unallocated() { +fn update_alliance_rewards_callback_with_unallocated() { let mut deps = mock_dependencies_with_balance(&[coin(2000000, "uluna")]); setup_contract(deps.as_mut()); set_alliance_asset(deps.as_mut()); @@ -283,7 +283,7 @@ fn update_reward_callback_with_unallocated() { assert_eq!( res, Response::new() - .add_attributes(vec![("action", "update_rewards_callback")]) + .add_attributes(vec![("action", "update_alliance_rewards_callback")]) .add_message(BankMsg::Send { to_address: "collector_address".to_string(), amount: vec![coin(300000, "uluna")] @@ -300,7 +300,7 @@ fn claim_user_rewards() { deps.as_mut(), Vec::from([ModifyAssetPair { asset_info: AssetInfo::Native("aWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }]), ); @@ -468,7 +468,7 @@ fn claim_user_rewards_after_staking() { deps.as_mut(), Vec::from([ModifyAssetPair { asset_info: AssetInfo::Native("aWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }]), ); @@ -546,12 +546,12 @@ fn claim_rewards_after_staking_and_unstaking() { Vec::from([ ModifyAssetPair { asset_info: AssetInfo::Native("aWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }, ModifyAssetPair { asset_info: AssetInfo::Native("bWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }, ]), @@ -636,14 +636,26 @@ fn claim_rewards_after_staking_and_unstaking() { stake(deps.as_mut(), "user1", 1000000, "aWHALE"); // User 1 should not have any rewards - let rewards = query_rewards(deps.as_ref(), "user1", "aWHALE","uluna"); - assert_eq!(rewards.rewards, Uint128::zero()); + let res = query_rewards(deps.as_ref(), "user1", "aWHALE","uluna"); + assert_eq!(res, PendingRewardsRes { + rewards: Uint128::zero(), + deposit_asset: AssetInfo::Native("aWHALE".to_string()), + reward_asset: AssetInfo::Native("uluna".to_string()), + }); // User 2 should receive all the rewards in the contract - let rewards = query_rewards(deps.as_ref(), "user2", "aWHALE","uluna"); - assert_eq!(rewards.rewards, Uint128::new(900000)); - let rewards = query_rewards(deps.as_ref(), "user2", "bWHALE","uluna"); - assert_eq!(rewards.rewards, Uint128::new(1000000)); + let res = query_rewards(deps.as_ref(), "user2", "aWHALE","uluna"); + assert_eq!(res, PendingRewardsRes { + rewards: Uint128::new(900000), + deposit_asset: AssetInfo::Native("aWHALE".to_string()), + reward_asset: AssetInfo::Native("uluna".to_string()), + }); + let res = query_rewards(deps.as_ref(), "user2", "bWHALE","uluna"); + assert_eq!(res, PendingRewardsRes { + rewards: Uint128::new(1000000), + deposit_asset: AssetInfo::Native("aWHALE".to_string()), + reward_asset: AssetInfo::Native("uluna".to_string()), + }); } #[test] @@ -656,12 +668,12 @@ fn claim_rewards_after_rebalancing_emissions() { Vec::from([ ModifyAssetPair { asset_info: AssetInfo::Native("aWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }, ModifyAssetPair { asset_info: AssetInfo::Native("bWHALE".to_string()), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }, ]), diff --git a/contracts/alliance-lp-hub/src/tests/stake_unstake.rs b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs index 4b055e6..28fc58c 100644 --- a/contracts/alliance-lp-hub/src/tests/stake_unstake.rs +++ b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs @@ -22,7 +22,7 @@ fn test_stake() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::native(Addr::unchecked("native_asset")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -97,7 +97,7 @@ fn test_stake_astro_token() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::native(Addr::unchecked("astro_existent_native_coin")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -144,7 +144,7 @@ fn test_stake_cw20() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -219,7 +219,7 @@ fn test_stake_astro_token_cw20() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::Cw20(Addr::unchecked("astro_existent_cw20")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -261,7 +261,7 @@ fn test_unstake() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::native("native_asset"), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -341,7 +341,7 @@ fn test_unstake_cw20_invalid() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); @@ -375,7 +375,7 @@ fn test_unstake_native_invalid() { deps.as_mut(), vec![ModifyAssetPair { asset_info: AssetInfo::native(Addr::unchecked("native_asset")), - reward_asset_info: None, + reward_asset_info: Some(AssetInfo::Native("uluna".to_string())), delete: false, }], ); diff --git a/packages/alliance-protocol/src/error.rs b/packages/alliance-protocol/src/error.rs index 21aa588..08dd908 100644 --- a/packages/alliance-protocol/src/error.rs +++ b/packages/alliance-protocol/src/error.rs @@ -1,6 +1,6 @@ use std::string::FromUtf8Error; -use cosmwasm_std::{Decimal, DecimalRangeExceeded, StdError}; +use cosmwasm_std::{CoinFromStrError, Decimal, DecimalRangeExceeded, StdError}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -14,6 +14,9 @@ pub enum ContractError { #[error("{0}")] DecimalRangeExceeded(#[from] DecimalRangeExceeded), + #[error("{0}")] + CoinFromStrError(#[from] CoinFromStrError), + #[error("Unauthorized")] Unauthorized {}, @@ -48,6 +51,9 @@ pub enum ContractError { #[error("Invalid contract callback with key: {0} and type: {1}")] InvalidContractCallback(String, String), + #[error("Invalid denom: {0}")] + InvalidDenom(String), + #[error("Missing reward asset info for asset {0}")] MissingRewardAsset(String), }