From 2599ecc48a097942135fdcdafb80cd747c4f446d Mon Sep 17 00:00:00 2001 From: emidev98 Date: Tue, 9 Jan 2024 18:04:40 +0200 Subject: [PATCH] wip(lp-hub): cw20 staking for both native and cw20 --- contracts/alliance-lp-hub/src/contract.rs | 21 +- .../alliance-lp-hub/src/tests/helpers.rs | 152 +++++++++- contracts/alliance-lp-hub/src/tests/mod.rs | 2 + .../src/tests/stake_unstake.rs | 281 ++++++++++++++++++ 4 files changed, 443 insertions(+), 13 deletions(-) create mode 100644 contracts/alliance-lp-hub/src/tests/stake_unstake.rs diff --git a/contracts/alliance-lp-hub/src/contract.rs b/contracts/alliance-lp-hub/src/contract.rs index e317901..d4af77b 100644 --- a/contracts/alliance-lp-hub/src/contract.rs +++ b/contracts/alliance-lp-hub/src/contract.rs @@ -84,9 +84,10 @@ pub fn execute( ExecuteMsg::ModifyAssets(assets) => modify_assets(deps, info, assets), ExecuteMsg::Receive(cw20_msg) => { + let sender = deps.api.addr_validate(&cw20_msg.sender)?; let received_asset = Asset::cw20(info.sender.clone(), cw20_msg.amount); - stake(deps, info, received_asset) + stake(deps, sender, received_asset) } ExecuteMsg::Stake {} => { if info.funds.len() != 1 { @@ -96,8 +97,9 @@ pub fn execute( if coin.amount.is_zero() { return Err(ContractError::AmountCannotBeZero {}); } - stake(deps, info, coin.into()) + stake(deps, info.sender, coin.into()) } + ExecuteMsg::Unstake(asset) => unstake(deps, info, asset), ExecuteMsg::ClaimRewards(asset) => claim_rewards(deps, info, asset), @@ -160,14 +162,13 @@ fn modify_assets( // update the user balance and the total balance for the asset. fn stake( deps: DepsMut, - info: MessageInfo, + sender: Addr, received_asset: Asset, ) -> Result { let asset_key = AssetInfoKey::from(&received_asset.info); WHITELIST .load(deps.storage, asset_key.clone()) .map_err(|_| ContractError::AssetNotWhitelisted {})?; - let sender = info.sender.clone(); let rewards = _claim_reward(deps.storage, sender.clone(), received_asset.info.clone())?; if !rewards.is_zero() { @@ -185,8 +186,8 @@ fn stake( (sender.clone(), asset_key.clone()), |balance| -> Result<_, ContractError> { match balance { - Some(balance) => Ok(balance + info.funds[0].amount), - None => Ok(info.funds[0].amount), + Some(balance) => Ok(balance + received_asset.amount), + None => Ok(received_asset.amount), } }, )?; @@ -194,20 +195,20 @@ fn stake( deps.storage, asset_key.clone(), |balance| -> Result<_, ContractError> { - Ok(balance.unwrap_or(Uint128::zero()) + info.funds[0].amount) + Ok(balance.unwrap_or(Uint128::zero()) + received_asset.amount) }, )?; let asset_reward_rate = ASSET_REWARD_RATE .load(deps.storage, asset_key.clone()) .unwrap_or(Decimal::zero()); - USER_ASSET_REWARD_RATE.save(deps.storage, (sender, asset_key), &asset_reward_rate)?; + USER_ASSET_REWARD_RATE.save(deps.storage, (sender.clone(), asset_key), &asset_reward_rate)?; Ok(Response::new().add_attributes(vec![ ("action", "stake"), - ("user", info.sender.as_ref()), + ("user", sender.as_ref()), ("asset", &received_asset.info.to_string()), - ("amount", &info.funds[0].amount.to_string()), + ("amount", &received_asset.amount.to_string()), ])) } diff --git a/contracts/alliance-lp-hub/src/tests/helpers.rs b/contracts/alliance-lp-hub/src/tests/helpers.rs index 44002d9..c0857f7 100644 --- a/contracts/alliance-lp-hub/src/tests/helpers.rs +++ b/contracts/alliance-lp-hub/src/tests/helpers.rs @@ -1,8 +1,21 @@ -use crate::contract::instantiate; -use crate::models::InstantiateMsg; +use crate::contract::{execute, instantiate}; +use crate::models::{ + AllPendingRewardsQuery, AssetQuery, Config, ExecuteMsg, InstantiateMsg, PendingRewardsRes, + QueryMsg, StakedBalanceRes, ModifyAsset, +}; +use crate::query::query; +use crate::state::CONFIG; +use alliance_protocol::alliance_protocol::{ + AllianceDelegateMsg, AllianceDelegation, AllianceRedelegateMsg, AllianceRedelegation, + AllianceUndelegateMsg, +}; use alliance_protocol::token_factory::CustomExecuteMsg; use cosmwasm_std::testing::{mock_env, mock_info}; -use cosmwasm_std::{DepsMut, Response}; +use cosmwasm_std::{coin, from_json, Deps, DepsMut, Response, StdResult, Uint128, Binary, Addr}; +use cw20::Cw20ReceiveMsg; +use cw_asset::{Asset, AssetInfo}; + +pub const DENOM: &str = "token_factory/token"; pub fn setup_contract(deps: DepsMut) -> Response { let info = mock_info("admin", &[]); @@ -15,3 +28,136 @@ pub fn setup_contract(deps: DepsMut) -> Response { }; instantiate(deps, env, info, init_msg).unwrap() } + +pub fn set_alliance_asset(deps: DepsMut) { + CONFIG + .update(deps.storage, |c| -> StdResult<_> { + Ok(Config { + alliance_token_denom: DENOM.to_string(), + alliance_token_supply: Uint128::new(1000000000000), + ..c + }) + }) + .unwrap(); +} + +pub fn modify_asset(deps: DepsMut, assets: Vec) -> Response { + let info = mock_info("gov", &[]); + let env = mock_env(); + + let msg = ExecuteMsg::ModifyAssets(assets); + execute(deps, env, info, msg).unwrap() +} + + +pub fn stake(deps: DepsMut, user: &str, amount: u128, denom: &str) -> Response { + let info = mock_info(user, &[coin(amount, denom)]); + let env = mock_env(); + let msg = ExecuteMsg::Stake {}; + execute(deps, env, info, msg).unwrap() +} + + +pub fn stake_cw20(deps: DepsMut, user: &str, amount: u128, denom: &str) -> Response { + let mut info = mock_info(user, &[]); + let env = mock_env(); + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg{ + sender: String::from(user), + amount: Uint128::new(amount), + msg: Binary::default(), + }); + info.sender = Addr::unchecked(denom.to_owned()); + execute(deps, env, info, msg).unwrap() +} + +pub fn unstake(deps: DepsMut, user: &str, amount: u128, denom: &str) -> Response { + let info = mock_info(user, &[]); + let env = mock_env(); + let msg = ExecuteMsg::Unstake(Asset::native(denom, amount)); + execute(deps, env, info, msg).unwrap() +} + +pub fn alliance_delegate(deps: DepsMut, delegations: Vec<(&str, u128)>) -> Response { + let info = mock_info("controller", &[]); + let env = mock_env(); + let delegations: Vec = delegations + .iter() + .map(|(addr, amount)| AllianceDelegation { + validator: addr.to_string(), + amount: Uint128::new(*amount), + }) + .collect(); + let msg = ExecuteMsg::AllianceDelegate(AllianceDelegateMsg { delegations }); + execute(deps, env, info, msg).unwrap() +} + +pub fn alliance_undelegate(deps: DepsMut, delegations: Vec<(&str, u128)>) -> Response { + let info = mock_info("controller", &[]); + let env = mock_env(); + let delegations: Vec = delegations + .iter() + .map(|(addr, amount)| AllianceDelegation { + validator: addr.to_string(), + amount: Uint128::new(*amount), + }) + .collect(); + let msg = ExecuteMsg::AllianceUndelegate(AllianceUndelegateMsg { + undelegations: delegations, + }); + execute(deps, env, info, msg).unwrap() +} + +pub fn alliance_redelegate(deps: DepsMut, redelegations: Vec<(&str, &str, u128)>) -> Response { + let info = mock_info("controller", &[]); + let env = mock_env(); + let redelegations: Vec = redelegations + .iter() + .map(|(src, dst, amount)| AllianceRedelegation { + src_validator: src.to_string(), + dst_validator: dst.to_string(), + amount: Uint128::new(*amount), + }) + .collect(); + let msg = ExecuteMsg::AllianceRedelegate(AllianceRedelegateMsg { redelegations }); + execute(deps, env, info, msg).unwrap() +} + +pub fn claim_rewards(deps: DepsMut, user: &str, denom: &str) -> Response { + let info = mock_info(user, &[]); + let env = mock_env(); + let msg = ExecuteMsg::ClaimRewards(AssetInfo::Native(denom.to_string())); + execute(deps, env, info, msg).unwrap() +} + +pub fn query_rewards(deps: Deps, user: &str, denom: &str) -> PendingRewardsRes { + from_json( + query( + deps, + mock_env(), + QueryMsg::PendingRewards(AssetQuery { + address: user.to_string(), + asset: AssetInfo::Native(denom.to_string()), + }), + ) + .unwrap(), + ) + .unwrap() +} + +pub fn query_all_rewards(deps: Deps, user: &str) -> Vec { + from_json( + query( + deps, + mock_env(), + QueryMsg::AllPendingRewards(AllPendingRewardsQuery { + address: user.to_string(), + }), + ) + .unwrap(), + ) + .unwrap() +} + +pub fn query_all_staked_balances(deps: Deps) -> Vec { + from_json(query(deps, mock_env(), QueryMsg::TotalStakedBalances {}).unwrap()).unwrap() +} diff --git a/contracts/alliance-lp-hub/src/tests/mod.rs b/contracts/alliance-lp-hub/src/tests/mod.rs index c0d0325..8eee544 100644 --- a/contracts/alliance-lp-hub/src/tests/mod.rs +++ b/contracts/alliance-lp-hub/src/tests/mod.rs @@ -1,2 +1,4 @@ mod helpers; mod instantiate; +mod stake_unstake; + diff --git a/contracts/alliance-lp-hub/src/tests/stake_unstake.rs b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs new file mode 100644 index 0000000..4931402 --- /dev/null +++ b/contracts/alliance-lp-hub/src/tests/stake_unstake.rs @@ -0,0 +1,281 @@ +use crate::contract::execute; +use crate::models::{ExecuteMsg, StakedBalanceRes, ModifyAsset}; +use crate::state::{BALANCES, TOTAL_BALANCES}; +use crate::tests::helpers::{ + query_all_staked_balances, setup_contract, stake, unstake, modify_asset, stake_cw20 +}; +use alliance_protocol::error::ContractError; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Response, Uint128, Decimal}; +use cw_asset::{Asset, AssetInfo, AssetInfoKey}; +use std::collections::HashMap; + +#[test] +fn test_stake() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + modify_asset( + deps.as_mut(), + 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, + } + ] + ); + + let res = stake(deps.as_mut(), "user1", 100, "native_asset"); + assert_eq!( + res, + Response::default().add_attributes(vec![ + ("action", "stake"), + ("user", "user1"), + ("asset", "native:native_asset"), + ("amount", "100"), + ]) + ); + + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Native("native_asset".to_string())), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(100)); + + // Stake more + let res = stake(deps.as_mut(), "user1", 100, "native_asset"); + assert_eq!( + res, + Response::default().add_attributes(vec![ + ("action", "stake"), + ("user", "user1"), + ("asset", "native:native_asset"), + ("amount", "100"), + ]) + ); + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Native("native_asset".to_string())), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(200)); + + let total_balance = TOTAL_BALANCES + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("native_asset".to_string())), + ) + .unwrap(); + assert_eq!(total_balance, Uint128::new(200)); + + let total_balances_res = query_all_staked_balances(deps.as_ref()); + assert_eq!( + total_balances_res, + vec![StakedBalanceRes { + asset: AssetInfo::Native("native_asset".to_string()), + balance: Uint128::new(200), + }] + ); +} + +#[test] +fn test_stake_cw20() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + modify_asset( + deps.as_mut(), + 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, + } + ] + ); + + let res = stake_cw20(deps.as_mut(), "user1", 100, "cw20_asset"); + assert_eq!( + res, + Response::default().add_attributes(vec![ + ("action", "stake"), + ("user", "user1"), + ("asset", "cw20:cw20_asset"), + ("amount", "100"), + ]) + ); + + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Cw20(Addr::unchecked("cw20_asset"))), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(100)); + + // Stake more + let res = stake_cw20(deps.as_mut(), "user1", 100, "cw20_asset"); + assert_eq!( + res, + Response::default().add_attributes(vec![ + ("action", "stake"), + ("user", "user1"), + ("asset", "cw20:cw20_asset"), + ("amount", "100"), + ]) + ); + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Cw20(Addr::unchecked("cw20_asset"))), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(200)); + + let total_balance = TOTAL_BALANCES + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Cw20(Addr::unchecked("cw20_asset"))), + ) + .unwrap(); + assert_eq!(total_balance, Uint128::new(200)); + + let total_balances_res = query_all_staked_balances(deps.as_ref()); + assert_eq!( + total_balances_res, + vec![StakedBalanceRes { + asset: AssetInfo::Cw20(Addr::unchecked("cw20_asset")), + balance: Uint128::new(200), + }] + ); +} + +#[test] +fn test_unstake() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + modify_asset( + deps.as_mut(), + 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, + } + ] + ); + stake_cw20(deps.as_mut(), "user1", 100, "cw20_asset"); + + let res = unstake(deps.as_mut(), "user1", 50, "cw20_asset"); + assert_eq!( + res, + Response::default() + .add_attributes(vec![ + ("action", "unstake"), + ("user", "user1"), + ("asset", "native:asset1"), + ("amount", "50"), + ]) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".into(), + amount: vec![coin(50, "asset1")], + })) + ); + + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Native("asset1".to_string())), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(50)); + + let res = unstake(deps.as_mut(), "user1", 50, "asset1"); + assert_eq!( + res, + Response::default() + .add_attributes(vec![ + ("action", "unstake"), + ("user", "user1"), + ("asset", "native:asset1"), + ("amount", "50"), + ]) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".into(), + amount: vec![coin(50, "asset1")], + })) + ); + + let balance = BALANCES + .load( + deps.as_ref().storage, + ( + Addr::unchecked("user1"), + AssetInfoKey::from(AssetInfo::Native("asset1".to_string())), + ), + ) + .unwrap(); + assert_eq!(balance, Uint128::new(0)); + + let total_balance = TOTAL_BALANCES + .load( + deps.as_ref().storage, + AssetInfoKey::from(AssetInfo::Native("asset1".to_string())), + ) + .unwrap(); + assert_eq!(total_balance, Uint128::new(0)); +} + +#[test] +fn test_unstake_invalid() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + modify_asset( + deps.as_mut(), + 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, + } + ] + ); + stake_cw20(deps.as_mut(), "user1", 100, "cw20_asset"); + + // User does not have any staked asset + let info = mock_info("user2", &[]); + let msg = ExecuteMsg::Unstake(Asset::native("cw20_asset", 100u128)); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::InsufficientBalance {}); + + // User unstakes more than they have + let info = mock_info("user1", &[]); + let msg = ExecuteMsg::Unstake(Asset::native("cw20_asset", 101u128)); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::InsufficientBalance {}); + + // User unstakes zero amount + let info = mock_info("user1", &[]); + let msg = ExecuteMsg::Unstake(Asset::native("cw20_asset", 0u128)); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::AmountCannotBeZero {}); +}