diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 3cf60854..01fed86a 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -177,6 +177,28 @@ "claim" ] }, + { + "description": "Claims the available rewards on behalf of the specified address. Only executable by the contract.", + "type": "object", + "required": [ + "claim_for_addr" + ], + "properties": { + "claim_for_addr": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Fills the contract with new rewards.", "type": "string", diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index 43d4f285..0cda7ce5 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -116,6 +116,28 @@ "claim" ] }, + { + "description": "Claims the available rewards on behalf of the specified address. Only executable by the contract.", + "type": "object", + "required": [ + "claim_for_addr" + ], + "properties": { + "claim_for_addr": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Fills the contract with new rewards.", "type": "string", diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index ca4a1dd5..217ab573 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -3,9 +3,10 @@ use cosmwasm_std::{ Uint128, }; -use white_whale_std::bonding_manager::Bond; +use white_whale_std::bonding_manager::{Bond, BondAction, Config, TemporalBondAction}; use white_whale_std::pool_network::asset; +use crate::helpers::temporal_bond_action_response; use crate::state::{ get_bonds_by_receiver, update_bond_weight, update_global_weight, BONDS, BOND_COUNTER, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, MAX_LIMIT, @@ -15,15 +16,19 @@ use crate::{helpers, ContractError}; /// Bonds the provided asset. pub(crate) fn bond( mut deps: DepsMut, - info: MessageInfo, - _env: Env, - asset: Coin, + info: &MessageInfo, + env: Env, + asset: &Coin, ) -> Result { helpers::validate_buckets_not_empty(&deps)?; - helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps)?; - let config = CONFIG.load(deps.storage)?; + + if let Some(temporal_bond_action_response) = + validate_bond_operation(&mut deps, info, &env, asset, &config, BondAction::Bond) + { + return temporal_bond_action_response; + } + let current_epoch: white_whale_std::epoch_manager::epoch_manager::EpochResponse = deps.querier.query_wasm_smart( config.epoch_manager_addr, @@ -101,17 +106,22 @@ pub(crate) fn bond( /// Unbonds the provided amount of tokens pub(crate) fn unbond( mut deps: DepsMut, - info: MessageInfo, + info: &MessageInfo, env: Env, - asset: Coin, + asset: &Coin, ) -> Result { ensure!( asset.amount > Uint128::zero(), ContractError::InvalidUnbondingAmount ); - helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps)?; + let config = CONFIG.load(deps.storage)?; + + if let Some(temporal_bond_action_response) = + validate_bond_operation(&mut deps, info, &env, asset, &config, BondAction::Unbond) + { + return temporal_bond_action_response; + } let bonds_by_receiver = get_bonds_by_receiver( deps.storage, @@ -138,7 +148,6 @@ pub(crate) fn unbond( ContractError::InsufficientBond ); - let config = CONFIG.load(deps.storage)?; let current_epoch: white_whale_std::epoch_manager::epoch_manager::EpochResponse = deps.querier.query_wasm_smart( config.epoch_manager_addr, @@ -242,3 +251,45 @@ pub(crate) fn withdraw( ("refund_amount", refund_amount.to_string()), ])) } + +/// Validates the bond operation. Makes sure the user has claimed pending rewards and that the current +/// epoch is valid. If any of these operations fail, the contract will resolve them by triggering +/// the claim rewards operation on behalf of the user, or by sending a message to the epoch manager +/// to create a new epoch. +/// +/// Used during both Bond and Unbond. +fn validate_bond_operation( + deps: &mut DepsMut, + info: &MessageInfo, + env: &Env, + asset: &Coin, + config: &Config, + bond_action: BondAction, +) -> Option> { + if helpers::validate_claimed(deps, info).is_err() { + return Some(temporal_bond_action_response( + deps, + &env.contract.address, + TemporalBondAction { + sender: info.sender.clone(), + coin: asset.clone(), + action: bond_action, + }, + ContractError::UnclaimedRewards, + )); + } + + if helpers::validate_bonding_for_current_epoch(deps, env).is_err() { + return Some(temporal_bond_action_response( + deps, + &config.epoch_manager_addr, + TemporalBondAction { + sender: info.sender.clone(), + coin: asset.clone(), + action: bond_action, + }, + ContractError::EpochNotCreatedYet, + )); + } + None +} diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index c282a13c..31b74fac 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -1,19 +1,25 @@ -use crate::error::ContractError; -use crate::helpers::{self, validate_growth_rate}; -use crate::state::{BONDING_ASSETS_LIMIT, BOND_COUNTER, CONFIG, UPCOMING_REWARD_BUCKET}; -use crate::{bonding, commands, queries, rewards}; use cosmwasm_std::{ensure, entry_point, Addr, Reply}; use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; use cw2::{get_contract_version, set_contract_version}; + use white_whale_std::bonding_manager::{ - Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, UpcomingRewardBucket, + BondAction, Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, TemporalBondAction, + UpcomingRewardBucket, +}; + +use crate::error::ContractError; +use crate::helpers::{self, validate_growth_rate}; +use crate::state::{ + BONDING_ASSETS_LIMIT, BOND_COUNTER, CONFIG, TMP_BOND_ACTION, UPCOMING_REWARD_BUCKET, }; +use crate::{bonding, commands, queries, rewards}; // version info for migration info const CONTRACT_NAME: &str = "white_whale-bonding_manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const LP_WITHDRAWAL_REPLY_ID: u64 = 0; +pub const NEW_EPOCH_CREATION_REPLY_ID: u64 = 1; #[entry_point] pub fn instantiate( @@ -59,9 +65,39 @@ pub fn instantiate( // Reply entrypoint handling LP withdraws from fill_rewards #[entry_point] -pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { match msg.id { LP_WITHDRAWAL_REPLY_ID => rewards::commands::handle_lp_withdrawal_reply(deps, msg), + NEW_EPOCH_CREATION_REPLY_ID => { + let TemporalBondAction { + sender, + coin, + action, + } = TMP_BOND_ACTION.load(deps.storage)?; + + TMP_BOND_ACTION.remove(deps.storage); + + match action { + BondAction::Bond => bonding::commands::bond( + deps, + &MessageInfo { + sender, + funds: vec![coin.clone()], + }, + env, + &coin, + ), + BondAction::Unbond => bonding::commands::unbond( + deps, + &MessageInfo { + sender, + funds: vec![], + }, + env, + &coin, + ), + } + } _ => Err(ContractError::Unauthorized), } } @@ -76,11 +112,11 @@ pub fn execute( match msg { ExecuteMsg::Bond => { let asset_to_bond = helpers::validate_funds(&deps, &info)?; - bonding::commands::bond(deps, info, env, asset_to_bond) + bonding::commands::bond(deps, &info, env, &asset_to_bond) } ExecuteMsg::Unbond { asset } => { cw_utils::nonpayable(&info)?; - bonding::commands::unbond(deps, info, env, asset) + bonding::commands::unbond(deps, &info, env, &asset) } ExecuteMsg::Withdraw { denom } => { cw_utils::nonpayable(&info)?; @@ -103,7 +139,25 @@ pub fn execute( ) } ExecuteMsg::FillRewards => rewards::commands::fill_rewards(deps, env, info), - ExecuteMsg::Claim => rewards::commands::claim(deps, info), + ExecuteMsg::Claim => { + cw_utils::nonpayable(&info)?; + rewards::commands::claim(deps, info) + } + ExecuteMsg::ClaimForAddr { address } => { + cw_utils::nonpayable(&info)?; + ensure!( + info.sender == env.contract.address, + ContractError::Unauthorized + ); + let sender = deps.api.addr_validate(&address)?; + rewards::commands::claim( + deps, + MessageInfo { + sender, + funds: vec![], + }, + ) + } ExecuteMsg::EpochChangedHook { current_epoch } => { rewards::commands::on_epoch_created(deps, info, current_epoch) } diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 83711b03..75a56337 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,13 +1,16 @@ use std::collections::{HashMap, VecDeque}; use cosmwasm_std::{ - ensure, to_json_binary, Addr, Attribute, Coin, CosmosMsg, Decimal, Deps, DepsMut, MessageInfo, - Order, ReplyOn, StdError, StdResult, SubMsg, Uint128, WasmMsg, + ensure, to_json_binary, wasm_execute, Addr, Attribute, Coin, CosmosMsg, Decimal, Deps, DepsMut, + Env, MessageInfo, Order, ReplyOn, Response, StdError, StdResult, SubMsg, Uint128, Uint64, + WasmMsg, }; use cw_utils::PaymentError; +use serde::Serialize; +use white_whale_std::bonding_manager::ExecuteMsg::ClaimForAddr; use white_whale_std::bonding_manager::{ - ClaimableRewardBucketsResponse, Config, GlobalIndex, RewardBucket, + ClaimableRewardBucketsResponse, Config, GlobalIndex, RewardBucket, TemporalBondAction, }; use white_whale_std::constants::LP_SYMBOL; use white_whale_std::epoch_manager::epoch_manager::EpochResponse; @@ -17,11 +20,12 @@ use white_whale_std::pool_manager::{ use white_whale_std::pool_network::asset; use white_whale_std::pool_network::asset::aggregate_coins; -use crate::contract::LP_WITHDRAWAL_REPLY_ID; +use crate::contract::{LP_WITHDRAWAL_REPLY_ID, NEW_EPOCH_CREATION_REPLY_ID}; use crate::error::ContractError; use crate::queries::query_claimable; use crate::state::{ - get_bonds_by_receiver, get_weight, CONFIG, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET, + get_bonds_by_receiver, get_weight, CONFIG, REWARD_BUCKETS, TMP_BOND_ACTION, + UPCOMING_REWARD_BUCKET, }; /// Validates that the growth rate is between 0 and 1. @@ -65,7 +69,7 @@ pub fn validate_funds(deps: &DepsMut, info: &MessageInfo) -> Result Result<(), ContractError> { // Do a smart query for Claimable let claimable_rewards: ClaimableRewardBucketsResponse = - query_claimable(&deps.as_ref(), Some(info.sender.to_string())).unwrap(); + query_claimable(&deps.as_ref(), Some(info.sender.to_string()))?; // ensure the user has nothing to claim ensure!( claimable_rewards.reward_buckets.is_empty(), @@ -77,13 +81,28 @@ pub fn validate_claimed(deps: &DepsMut, info: &MessageInfo) -> Result<(), Contra /// Validates that the current time is not more than a day after the epoch start time. Helps preventing /// global_index timestamp issues when querying the weight. -pub fn validate_bonding_for_current_epoch(deps: &DepsMut) -> Result<(), ContractError> { +pub fn validate_bonding_for_current_epoch(deps: &DepsMut, env: &Env) -> Result<(), ContractError> { let config = CONFIG.load(deps.storage)?; let epoch_response: EpochResponse = deps.querier.query_wasm_smart( config.epoch_manager_addr.to_string(), &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, )?; + let epoch_manager_config: white_whale_std::epoch_manager::epoch_manager::ConfigResponse = + deps.querier.query_wasm_smart( + config.epoch_manager_addr.to_string(), + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::Config {}, + )?; + + // Ensure that the current time is not more than the epoch duration after the epoch start time, + // otherwise it means a new epoch must be created before the user can bond. + ensure!( + Uint64::new(env.block.time.nanos()) + .checked_sub(Uint64::new(epoch_response.epoch.start_time.nanos()))? + < epoch_manager_config.epoch_config.duration, + ContractError::EpochNotCreatedYet + ); + let reward_bucket = REWARD_BUCKETS.may_load(deps.storage, epoch_response.epoch.id)?; ensure!(reward_bucket.is_some(), ContractError::EpochNotCreatedYet); @@ -478,3 +497,46 @@ pub fn fill_upcoming_reward_bucket(deps: DepsMut, funds: Coin) -> StdResult<()> Ok(()) } + +/// Creates a [SubMsg] for the given [TemporalBondAction]. +pub fn temporal_bond_action_response( + deps: &mut DepsMut, + contract_addr: &Addr, + temporal_bond_action: TemporalBondAction, + error: ContractError, +) -> Result { + TMP_BOND_ACTION.save(deps.storage, &temporal_bond_action)?; + + let submsg = match error { + ContractError::UnclaimedRewards => create_temporal_bond_action_submsg( + contract_addr, + &ClaimForAddr { + address: temporal_bond_action.sender.to_string(), + }, + )?, + ContractError::EpochNotCreatedYet => create_temporal_bond_action_submsg( + contract_addr, + &white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch, + )?, + _ => panic!("Can't enter here. Invalid error"), + }; + + Ok(Response::default() + .add_submessage(submsg) + .add_attributes(vec![("action", temporal_bond_action.action.to_string())])) +} + +/// If there is a new epoch to be created, creates a [SubMsg] to create a new epoch. Used to trigger +/// epoch creation when the user is bonding/unbonding and the epoch has not been created yet. +/// +/// If there are unclaimed rewards, creates a [SubMsg] to claim rewards. Used to trigger when the +/// user is bonding/unbonding, and it hasn't claimed pending rewards yet. +fn create_temporal_bond_action_submsg( + contract_addr: &Addr, + msg: &impl Serialize, +) -> Result { + Ok(SubMsg::reply_on_success( + wasm_execute(contract_addr, msg, vec![])?, + NEW_EPOCH_CREATION_REPLY_ID, + )) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index a085f1f3..d2efe920 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -2,13 +2,14 @@ use cosmwasm_std::{Addr, Decimal, DepsMut, Order, StdError, StdResult, Storage, use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use white_whale_std::bonding_manager::{ - Bond, Config, GlobalIndex, RewardBucket, UpcomingRewardBucket, + Bond, Config, GlobalIndex, RewardBucket, TemporalBondAction, UpcomingRewardBucket, }; use crate::ContractError; pub const BONDING_ASSETS_LIMIT: usize = 2; pub const CONFIG: Item = Item::new("config"); +pub const TMP_BOND_ACTION: Item = Item::new("temporal_bond_action"); /// A monotonically increasing counter to generate unique bond ids. pub const BOND_COUNTER: Item = Item::new("bond_counter"); diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index 11e825dd..cacc334d 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -80,3 +80,60 @@ fn test_same_bond_multiple_times() { ); }); } + +#[test] +fn test_bonding_unbonding_without_creating_new_epoch_on_time() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); + + suite + .instantiate_default() + .add_one_day() + .create_new_epoch() + .fast_forward(86_399) + // bonds the last second before the new epoch kicks in + .bond( + creator.clone(), + &vec![coin(1_000u128, "bWHALE")], + |result| { + result.unwrap(); + }, + ) + .fast_forward(1) + // tries to bond when the new epoch should have been created, but since it wasn't it is triggered + // by the contract via a submsg/reply + .query_current_epoch(|res| { + assert_eq!(res.unwrap().1.epoch.id, 1u64); + }) + .bond( + creator.clone(), + &vec![coin(2_000u128, "bWHALE")], + |result| { + result.unwrap(); + }, + ) + .query_current_epoch(|res| { + assert_eq!(res.unwrap().1.epoch.id, 2u64); + }) + .query_bonded(Some(creator.clone().to_string()), |res| { + assert_eq!( + res.unwrap().1.bonded_assets, + vec![coin(3_000u128, "bWHALE")] + ); + }); + + // now try unbonding + + suite + .add_one_day() + .fast_forward(60) //one minute past when the new epoch should have been created + .query_current_epoch(|res| { + assert_eq!(res.unwrap().1.epoch.id, 2u64); + }) + .unbond(creator.clone(), coin(3_000u128, "bWHALE"), |result| { + result.unwrap(); + }) + .query_current_epoch(|res| { + assert_eq!(res.unwrap().1.epoch.id, 3u64); + }); +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs index 9b1308bb..9d6d93dd 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -498,12 +498,9 @@ fn test_claim_successfully() { another_sender.clone(), &coins(1_000u128, "bWHALE"), |result| { - let err = result.unwrap_err().downcast::().unwrap(); - - match err { - ContractError::UnclaimedRewards { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), - } + // this one had pending claims, but the contract should claim them automatically + // on behalf of the user + result.unwrap(); }, ) .bond( @@ -556,7 +553,7 @@ fn test_claim_successfully() { claimed: vec![], global_index: GlobalIndex { epoch_id: 6, - bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_amount: Uint128::new(1_000 + 5_000 + 700 + 1000), bonded_assets: vec![ Coin { denom: "ampWHALE".to_string(), @@ -564,11 +561,11 @@ fn test_claim_successfully() { }, Coin { denom: "bWHALE".to_string(), - amount: Uint128::new(5_000 + 700), + amount: Uint128::new(5_000 + 700 + 1000), }, ], last_updated: 5, - last_weight: Uint128::new(9_400), + last_weight: Uint128::new(9_400 + 1000), }, }, RewardBucket { @@ -580,9 +577,12 @@ fn test_claim_successfully() { }], available: vec![Coin { denom: "uwhale".to_string(), - amount: Uint128::new(999), + amount: Uint128::new(999 - 317), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(317), }], - claimed: vec![], global_index: GlobalIndex { epoch_id: 5, bonded_amount: Uint128::new(1_000 + 700), @@ -669,17 +669,17 @@ fn test_claim_successfully() { .query_rewards(creator.clone().to_string(), |res| { let (_, rewards) = res.unwrap(); assert_eq!(rewards.rewards.len(), 1); - assert_eq!(rewards.rewards[0].amount, Uint128::new(1078u128)); + assert_eq!(rewards.rewards[0].amount, Uint128::new(1056u128)); }) .query_rewards(another_sender.clone().to_string(), |res| { let (_, rewards) = res.unwrap(); assert_eq!(rewards.rewards.len(), 1); - assert_eq!(rewards.rewards[0].amount, Uint128::new(421u128)); + assert_eq!(rewards.rewards[0].amount, Uint128::new(180u128)); }) .query_rewards(yet_another_sender.clone().to_string(), |res| { let (_, rewards) = res.unwrap(); assert_eq!(rewards.rewards.len(), 1); - assert_eq!(rewards.rewards[0].amount, Uint128::new(496u128)); + assert_eq!(rewards.rewards[0].amount, Uint128::new(441u128)); }); // let's claim now @@ -705,16 +705,18 @@ fn test_claim_successfully() { suite .bond(creator.clone(), &coins(1_000u128, "bWHALE"), |result| { + // this one had pending claims, but the contract should claim them automatically + // on behalf of the user + result.unwrap(); + }) + .claim(creator.clone(), |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::UnclaimedRewards { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), + ContractError::NothingToClaim { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NothingToClaim"), } }) - .claim(creator.clone(), |result| { - result.unwrap(); - }) .bond(creator.clone(), &coins(1_000u128, "bWHALE"), |result| { result.unwrap(); }) @@ -736,13 +738,13 @@ fn test_claim_successfully() { suite .query_balance("uwhale".to_string(), creator.clone(), |balance| { assert_eq!( - creator_balance.clone().into_inner() + Uint128::new(1078u128), + creator_balance.clone().into_inner() + Uint128::new(1056u128), balance ); }) .query_balance("uwhale".to_string(), another_sender.clone(), |balance| { assert_eq!( - another_sender_balance.clone().into_inner() + Uint128::new(421u128), + another_sender_balance.clone().into_inner() + Uint128::new(180u128), balance ); }) @@ -751,7 +753,7 @@ fn test_claim_successfully() { yet_another_sender.clone(), |balance| { assert_eq!( - yet_another_sender_balance.clone().into_inner() + Uint128::new(496u128), + yet_another_sender_balance.clone().into_inner() + Uint128::new(441u128), balance ); }, @@ -772,15 +774,15 @@ fn test_claim_successfully() { }], available: vec![Coin { denom: "uwhale".to_string(), - amount: Uint128::new(1), + amount: Uint128::new(2), }], claimed: vec![Coin { denom: "uwhale".to_string(), - amount: Uint128::new(798), + amount: Uint128::new(797), }], global_index: GlobalIndex { epoch_id: 6, - bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_amount: Uint128::new(1_000 + 5_000 + 700 + 1000), bonded_assets: vec![ Coin { denom: "ampWHALE".to_string(), @@ -788,11 +790,11 @@ fn test_claim_successfully() { }, Coin { denom: "bWHALE".to_string(), - amount: Uint128::new(5_000 + 700), + amount: Uint128::new(5_000 + 700 + 1000), }, ], last_updated: 5, - last_weight: Uint128::new(9_400), + last_weight: Uint128::new(9_400 + 1000), }, }, RewardBucket { diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs b/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs index 445d0864..7bc0e843 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs @@ -40,7 +40,7 @@ fn test_queries() { }); suite - .fast_forward(259_200) + .add_one_day() // epoch 1 .create_new_epoch() .create_pair( @@ -95,6 +95,7 @@ fn test_queries() { result.unwrap(); }, ) + .add_one_day() // epoch 2 .create_new_epoch() .bond(creator.clone(), &coins(1_000u128, "ampWHALE"), |res| { @@ -134,7 +135,7 @@ fn test_queries() { ); // epoch 3 - suite.create_new_epoch(); + suite.add_one_day().create_new_epoch(); suite .query_global_index(None, |result| { diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index f56f2912..ecb2cd51 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -14,7 +14,7 @@ use white_whale_std::bonding_manager::{ UnbondingResponse, WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; -use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; +use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig, EpochResponse}; use white_whale_std::pool_manager::PoolType; pub fn bonding_manager_contract() -> Box> { @@ -582,6 +582,27 @@ impl TestingSuite { self } + // Epoch Manager queries + + #[track_caller] + pub(crate) fn query_current_epoch( + &mut self, + response: impl Fn(StdResult<(&mut Self, EpochResponse)>), + ) -> &mut Self { + let epoch_response: EpochResponse = self + .app + .wrap() + .query_wasm_smart( + &self.epoch_manager_addr, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch, + ) + .unwrap(); + + response(Ok((self, epoch_response))); + + self + } + // Pool Manager methods #[track_caller] diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs b/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs index 4bf30267..19d8f65c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs @@ -436,17 +436,7 @@ fn test_unbonding_withdraw() { suite .unbond(creator.clone(), coin(1_000u128, "bWHALE"), |result| { - let err = result.unwrap_err().downcast::().unwrap(); - // can't unbond if there are rewards to claim - match err { - ContractError::UnclaimedRewards { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), - } - }) - .claim(creator.clone(), |result| { - result.unwrap(); - }) - .unbond(creator.clone(), coin(1_000u128, "bWHALE"), |result| { + // this is claiming the pending rewards, and then trying to unbond the asset it doesn't have let err = result.unwrap_err().downcast::().unwrap(); // can't unbond an asset the user never bonded match err { @@ -699,21 +689,25 @@ fn test_unbonding_withdraw() { .create_new_epoch(); suite - .unbond(another_sender.clone(), coin(700u128, "bWHALE"), |result| { - let err = result.unwrap_err().downcast::().unwrap(); - // can't unbond if there are rewards to claim - match err { - ContractError::UnclaimedRewards { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), - } + .query_rewards(another_sender.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + assert_eq!(rewards.rewards.len(), 1); + assert_eq!(rewards.rewards[0].amount, Uint128::new(539u128)); }) - .claim(another_sender.clone(), |result| { - result.unwrap(); + .query_balance("uwhale".to_string(), another_sender.clone(), |balance| { + *another_sender_balance.borrow_mut() = balance; }) .unbond(another_sender.clone(), coin(300u128, "bWHALE"), |result| { + // Claims pending rewards // partial unbond result.unwrap(); }) + .query_balance("uwhale".to_string(), another_sender.clone(), |balance| { + assert_eq!( + another_sender_balance.clone().into_inner() + Uint128::new(539u128), + balance + ); + }) .fast_forward(1) .unbond(another_sender.clone(), coin(200u128, "bWHALE"), |result| { // partial unbond @@ -973,17 +967,7 @@ fn test_unbonding_withdraw() { ); }) .unbond(another_sender.clone(), coin(200u128, "bWHALE"), |result| { - let err = result.unwrap_err().downcast::().unwrap(); - // can't unbond if there are rewards to claim - match err { - ContractError::UnclaimedRewards { .. } => {} - _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), - } - }) - .claim(another_sender.clone(), |result| { - result.unwrap(); - }) - .unbond(another_sender.clone(), coin(200u128, "bWHALE"), |result| { + // This will claim the pending rewards // total unbond result.unwrap(); }); diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 535b0ce7..fc098f36 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -1,4 +1,5 @@ use crate::epoch_manager::epoch_manager::Epoch; +use std::fmt::Display; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ @@ -154,6 +155,8 @@ pub enum ExecuteMsg { }, /// Claims the available rewards Claim, + /// Claims the available rewards on behalf of the specified address. Only executable by the contract. + ClaimForAddr { address: String }, /// Fills the contract with new rewards. FillRewards, /// Epoch Changed hook implementation. Creates a new reward bucket for the rewards flowing from @@ -263,6 +266,28 @@ pub struct ClaimableRewardBucketsResponse { pub reward_buckets: Vec, } +#[cw_serde] +pub struct TemporalBondAction { + pub sender: Addr, + pub coin: Coin, + pub action: BondAction, +} + +#[cw_serde] +pub enum BondAction { + Bond, + Unbond, +} + +impl Display for BondAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BondAction::Bond => write!(f, "bond"), + BondAction::Unbond => write!(f, "unbond"), + } + } +} + /// Creates a message to fill rewards on the whale lair contract. pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult { Ok(CosmosMsg::Wasm(WasmMsg::Execute {