From e3336f575bcf523603e2842f751753902bcb61a7 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 16 May 2024 17:05:26 +0100 Subject: [PATCH] test: add unbond and withdraw tests --- .../schema/bonding-manager.json | 39 +- .../bonding-manager/schema/raw/query.json | 6 +- .../schema/raw/response_to_claimable.json | 10 +- .../schema/raw/response_to_global_index.json | 10 +- .../schema/raw/response_to_unbonding.json | 13 +- .../bonding-manager/src/bonding/commands.rs | 16 +- .../bonding-manager/src/contract.rs | 6 +- .../bonding-manager/src/helpers.rs | 9 +- .../bonding-manager/src/queries.rs | 50 +- .../bonding-manager/src/rewards/commands.rs | 16 - .../bonding-manager/src/state.rs | 4 - .../bonding-manager/src/tests/bond.rs | 10 +- .../bonding-manager/src/tests/claim.rs | 2 +- .../bonding-manager/src/tests/helpers.rs | 1 - .../bonding-manager/src/tests/instantiate.rs | 2 +- .../bonding-manager/src/tests/mod.rs | 6 +- .../bonding-manager/src/tests/queries.rs | 369 +++++ .../bonding-manager/src/tests/suite.rs | 23 +- .../bonding-manager/src/tests/unbond.rs | 445 ------ .../src/tests/unbond_withdraw.rs | 1386 +++++++++++++++++ .../bonding-manager/src/tests/withdraw.rs | 113 -- .../white-whale-std/src/bonding_manager.rs | 34 +- packages/white-whale-std/src/coin.rs | 5 +- 23 files changed, 1848 insertions(+), 727 deletions(-) create mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/queries.rs delete mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs create mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs delete mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 9d4a045a..8fe857a7 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -488,7 +488,7 @@ "additionalProperties": false }, { - "description": "Returns the weight of the address. Returns the global index of the contract.", + "description": "Returns the global index of the contract.", "type": "object", "required": [ "global_index" @@ -497,8 +497,8 @@ "global_index": { "type": "object", "properties": { - "epoch_id": { - "description": "The epoch id to check for the global index. If none is provided, the current global index is returned.", + "reward_bucket_id": { + "description": "The reward bucket id to check for the global index. If none is provided, the current global index is returned.", "type": [ "integer", "null" @@ -678,8 +678,8 @@ "bonded_amount", "bonded_assets", "epoch_id", - "updated_last", - "weight" + "last_updated", + "last_weight" ], "properties": { "bonded_amount": { @@ -703,14 +703,14 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "weight": { - "description": "The total weight of the bond at the given block height.", + "last_weight": { + "description": "The total weight of the contract at the given updated_last epoch id.", "allOf": [ { "$ref": "#/definitions/Uint128" @@ -863,8 +863,8 @@ "bonded_amount", "bonded_assets", "epoch_id", - "updated_last", - "weight" + "last_updated", + "last_weight" ], "properties": { "bonded_amount": { @@ -888,14 +888,14 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "weight": { - "description": "The total weight of the bond at the given block height.", + "last_weight": { + "description": "The total weight of the contract at the given updated_last epoch id.", "allOf": [ { "$ref": "#/definitions/Uint128" @@ -1094,7 +1094,7 @@ "required": [ "asset", "created_at_epoch", - "updated_last", + "last_updated", "weight" ], "properties": { @@ -1112,12 +1112,21 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the bond was last time updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, + "unbonded_at": { + "description": "The time at which the Bond was unbonded.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "weight": { "description": "The weight of the bond at the given block height.", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index 376bbcb4..1dae1f1d 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -107,7 +107,7 @@ "additionalProperties": false }, { - "description": "Returns the weight of the address. Returns the global index of the contract.", + "description": "Returns the global index of the contract.", "type": "object", "required": [ "global_index" @@ -116,8 +116,8 @@ "global_index": { "type": "object", "properties": { - "epoch_id": { - "description": "The epoch id to check for the global index. If none is provided, the current global index is returned.", + "reward_bucket_id": { + "description": "The reward bucket id to check for the global index. If none is provided, the current global index is returned.", "type": [ "integer", "null" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json index 351a5b7a..788b95ed 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json @@ -37,8 +37,8 @@ "bonded_amount", "bonded_assets", "epoch_id", - "updated_last", - "weight" + "last_updated", + "last_weight" ], "properties": { "bonded_amount": { @@ -62,14 +62,14 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "weight": { - "description": "The total weight of the bond at the given block height.", + "last_weight": { + "description": "The total weight of the contract at the given updated_last epoch id.", "allOf": [ { "$ref": "#/definitions/Uint128" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_global_index.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_global_index.json index 66367c40..6b598589 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_global_index.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_global_index.json @@ -6,8 +6,8 @@ "bonded_amount", "bonded_assets", "epoch_id", - "updated_last", - "weight" + "last_updated", + "last_weight" ], "properties": { "bonded_amount": { @@ -31,14 +31,14 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "weight": { - "description": "The total weight of the bond at the given block height.", + "last_weight": { + "description": "The total weight of the contract at the given updated_last epoch id.", "allOf": [ { "$ref": "#/definitions/Uint128" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json index ffa51158..5181a615 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json @@ -31,7 +31,7 @@ "required": [ "asset", "created_at_epoch", - "updated_last", + "last_updated", "weight" ], "properties": { @@ -49,12 +49,21 @@ "format": "uint64", "minimum": 0.0 }, - "updated_last": { + "last_updated": { "description": "The epoch id at which the bond was last time updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, + "unbonded_at": { + "description": "The time at which the Bond was unbonded.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "weight": { "description": "The weight of the bond at the given block height.", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index 4c91fc10..7f022c52 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -17,9 +17,7 @@ pub(crate) fn bond( _env: Env, asset: Coin, ) -> Result { - println!("----bond----"); helpers::validate_buckets_not_empty(&deps)?; - //todo maybe claim for the user helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps)?; @@ -43,7 +41,7 @@ pub(crate) fn bond( ..Bond::default() }); - // update local values + // update bond values bond = update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; @@ -52,12 +50,8 @@ pub(crate) fn bond( // update global values let mut global_index = GLOBAL.load(deps.storage)?; - // include time term in the weight - - println!("bonding global_index: {:?}", global_index); global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index.clone())?; - global_index.last_weight = global_index.last_weight.checked_add(asset.amount)?; global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; global_index.bonded_assets = @@ -105,7 +99,7 @@ pub(crate) fn unbond( &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, )?; - // update local values, decrease the bond + // update bond values, decrease the bond unbond = update_bond_weight( &mut deps, info.sender.clone(), @@ -125,13 +119,13 @@ pub(crate) fn unbond( // record the unbonding UNBOND.save( deps.storage, - (&info.sender, &asset.denom, env.block.time.nanos()), + (&info.sender, &asset.denom, env.block.time.seconds()), &Bond { asset: asset.clone(), weight: Uint128::zero(), last_updated: current_epoch.epoch.id, created_at_epoch: current_epoch.epoch.id, - //previous: None, + unbonded_at: Some(env.block.time.seconds()), }, )?; // update global values @@ -186,6 +180,8 @@ pub(crate) fn withdraw( } } + ensure!(!refund_amount.is_zero(), ContractError::NothingToWithdraw); + let refund_msg = CosmosMsg::Bank(BankMsg::Send { to_address: address.to_string(), amount: vec![Coin { diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index bd17d9fb..4afb321c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -158,9 +158,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary(&queries::query_global_index( - deps, epoch_id, - )?)?), + QueryMsg::GlobalIndex { reward_bucket_id } => Ok(to_json_binary( + &queries::query_global_index(deps, reward_bucket_id)?, + )?), QueryMsg::Claimable { address } => { Ok(to_json_binary(&queries::query_claimable(&deps, address)?)?) } diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 7e6390e7..e1513a14 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -90,24 +90,19 @@ pub fn validate_bonding_for_current_epoch(deps: &DepsMut) -> Result<(), Contract // If we do get some LP tokens to withdraw they could be swapped to whale in the reply pub fn handle_lp_tokens_rewards( deps: &DepsMut, - funds: &Vec, + funds: &[Coin], config: &Config, submessages: &mut Vec, ) -> Result<(), ContractError> { - println!("funds: {:?}", funds); let lp_tokens: Vec<&Coin> = funds .iter() .filter(|coin| coin.denom.contains(".pool.") | coin.denom.contains(LP_SYMBOL)) .collect(); - println!("lp_tokens: {:?}", lp_tokens); - for lp_token in lp_tokens { let pool_identifier = extract_pool_identifier(&lp_token.denom).ok_or(ContractError::AssetMismatch)?; - println!("pool_identifier: {:?}", pool_identifier); - // make sure a pool with the given identifier exists let pool: StdResult = deps.querier.query_wasm_smart( config.pool_manager_addr.to_string(), @@ -305,7 +300,7 @@ pub fn calculate_rewards( deps, reward_bucket.id, address.to_string(), - Some(reward_bucket.global_index.clone()), + reward_bucket.global_index.clone(), )?; // sanity check, if the user has no share in the bucket, skip it diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 423658f0..c8a595f0 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,6 +1,6 @@ use std::collections::VecDeque; -use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Decimal, Deps, Order, StdResult, Uint128}; use cw_storage_plus::Bound; use crate::{helpers, ContractError}; @@ -151,7 +151,7 @@ pub(crate) fn query_weight( deps: &Deps, epoch_id: u64, address: String, - global_index: Option, + mut global_index: GlobalIndex, ) -> StdResult { let address = deps.api.addr_validate(&address)?; @@ -161,15 +161,10 @@ pub(crate) fn query_weight( .take(MAX_PAGE_LIMIT as usize) .collect(); - println!("----query_weight----"); - println!("bonds: {:?}", bonds); - let config = CONFIG.load(deps.storage)?; let mut total_bond_weight = Uint128::zero(); - println!("epoch id: {:?}", epoch_id); - for (_, mut bond) in bonds? { bond.weight = get_weight( epoch_id, @@ -182,22 +177,8 @@ pub(crate) fn query_weight( // Aggregate the weights of all the bonds for the given address. // This assumes bonding assets are fungible. total_bond_weight = total_bond_weight.checked_add(bond.weight)?; - println!("total_bond_weight: {:?}", total_bond_weight); } - // If a global weight from an Epoch was passed, use that to get the weight, otherwise use the current global index weight - let mut global_index = if let Some(global_index) = global_index { - global_index - } else { - println!("here?"); - GLOBAL - .may_load(deps.storage) - .unwrap_or_else(|_| Some(GlobalIndex::default())) - .ok_or_else(|| StdError::generic_err("Global index not found"))? - }; - - println!("global_index: {:?}", global_index); - global_index.last_weight = get_weight( epoch_id, global_index.last_weight, @@ -206,8 +187,6 @@ pub(crate) fn query_weight( global_index.last_updated, )?; - println!("global_index--after: {:?}", global_index); - // Represents the share of the global weight that the address has // If global_index.weight is zero no one has bonded yet so the share is let share = if global_index.last_weight.is_zero() { @@ -216,8 +195,6 @@ pub(crate) fn query_weight( Decimal::from_ratio(total_bond_weight, global_index.last_weight) }; - println!("share: {:?}", share); - Ok(BondingWeightResponse { address: address.to_string(), weight: total_bond_weight, @@ -228,13 +205,15 @@ pub(crate) fn query_weight( } /// Queries the global index -pub fn query_global_index(deps: Deps, epoch_id: Option) -> StdResult { - // if an epoch_id is provided, return the global index of the corresponding reward bucket - if let Some(epoch_id) = epoch_id { - let reward_bucket = REWARD_BUCKETS.may_load(deps.storage, epoch_id)?; - if let Some(reward_bucket) = reward_bucket { - return Ok(reward_bucket.global_index); - } +pub fn query_global_index(deps: Deps, reward_bucket_id: Option) -> StdResult { + // if a reward_bucket_id is provided, return the global index of the corresponding reward bucket + if let Some(reward_bucket_id) = reward_bucket_id { + let reward_bucket = REWARD_BUCKETS.may_load(deps.storage, reward_bucket_id)?; + return if let Some(reward_bucket) = reward_bucket { + Ok(reward_bucket.global_index) + } else { + Ok(GlobalIndex::default()) + }; } let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); @@ -298,17 +277,12 @@ pub fn query_claimable( address: Option, ) -> StdResult { let mut claimable_reward_buckets = get_claimable_reward_buckets(deps)?.reward_buckets; - println!("fable"); - println!("claimable_reward_buckets: {:?}", claimable_reward_buckets); - // if an address is provided, filter what's claimable for that address if let Some(address) = address { let address = deps.api.addr_validate(&address)?; let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &address)?; - println!("last_claimed_epoch: {:?}", last_claimed_epoch); - // filter out buckets that have already been claimed by the user if let Some(last_claimed_epoch) = last_claimed_epoch { claimable_reward_buckets.retain(|bucket| bucket.id > last_claimed_epoch); @@ -322,8 +296,6 @@ pub fn query_claimable( claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } - println!("claimable_reward_buckets: {:?}", claimable_reward_buckets); - Ok(ClaimableRewardBucketsResponse { reward_buckets: claimable_reward_buckets, }) diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs index 992fec00..444c2f36 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -25,11 +25,6 @@ pub(crate) fn on_epoch_created( ) -> Result { cw_utils::nonpayable(&info)?; - println!( - ">>>>>>>>>>>>>>>>>>> {:?}{:?}{:?}", - current_epoch.id, current_epoch.id, current_epoch.id - ); - println!("EpochChangedHook: {:?}", current_epoch); // A new epoch has been created, update rewards bucket and forward the expiring bucket let config = CONFIG.load(deps.storage)?; ensure!( @@ -58,8 +53,6 @@ pub(crate) fn on_epoch_created( GLOBAL.save(deps.storage, &global_index)?; - println!("--- global_index: {:?}", global_index); - // Create a new reward bucket for the current epoch with the total rewards accrued in the // upcoming bucket item let upcoming_bucket = UPCOMING_REWARD_BUCKET.load(deps.storage)?; @@ -103,8 +96,6 @@ pub(crate) fn fill_rewards( env: Env, info: MessageInfo, ) -> Result { - println!("----fill_rewards----"); - let config = CONFIG.load(deps.storage)?; let distribution_denom = config.distribution_denom.clone(); @@ -159,8 +150,6 @@ pub(crate) fn fill_rewards( /// Handles the lp withdrawal reply. It will swap the non-distribution denom coins to the /// distribution denom and aggregate the funds to the upcoming reward bucket. pub fn handle_lp_withdrawal_reply(deps: DepsMut, msg: Reply) -> Result { - println!("---handle_lp_withdrawal_reply---"); - // Read the coins sent via data on the withdraw response of the pool manager let execute_contract_response = parse_reply_execute_data(msg.clone()).unwrap(); let data = execute_contract_response @@ -172,7 +161,6 @@ pub fn handle_lp_withdrawal_reply(deps: DepsMut, msg: Reply) -> Result Result Result().unwrap(); + // the system has not been initialized + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }); + + suite + .fast_forward(259_200) + // epoch 1 + .create_new_epoch() + .create_pair( + creator.clone(), + asset_denoms.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uusdc".to_string(), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1_000_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .swap( + creator.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + // epoch 2 + .create_new_epoch() + .bond(creator.clone(), &coins(1_000u128, "ampWHALE"), |res| { + res.unwrap(); + }) + .swap( + creator.clone(), + coin(2_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(2_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + // epoch 3 + suite.create_new_epoch(); + + suite + .query_global_index(None, |result| { + let global_index = result.unwrap().1; + assert_eq!( + global_index, + GlobalIndex { + epoch_id: 3, + bonded_amount: Uint128::new(1_000), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::from(1_000u128), + }], + last_updated: 2, + last_weight: Uint128::from(1_000u128), + } + ); + }) + .query_global_index(Some(3u64), |result| { + let global_index = result.unwrap().1; + assert_eq!( + global_index, + GlobalIndex { + epoch_id: 3, + bonded_amount: Uint128::new(1_000), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::from(1_000u128), + }], + last_updated: 2, + last_weight: Uint128::from(1_000u128), + } + ); + }) + .query_global_index(Some(2u64), |result| { + let global_index = result.unwrap().1; + assert_eq!( + global_index, + GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + } + ); + }) + .query_global_index(Some(1u64), |result| { + let global_index = result.unwrap().1; + assert_eq!( + global_index, + GlobalIndex { + epoch_id: 1, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 1, + last_weight: Default::default(), + } + ); + }) + .query_bonded(None, |result| { + let bonded_response = result.unwrap().1; + assert_eq!( + bonded_response, + BondedResponse { + total_bonded: Uint128::new(1_000u128), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::from(1_000u128), + }], + first_bonded_epoch_id: None, + } + ); + }); + + suite.claim(creator.clone(), |result| { + result.unwrap(); + }); + + suite + .unbond(creator.clone(), coin(100u128, "ampWHALE"), |result| { + result.unwrap(); + }) + .fast_forward(1_000) + .unbond(creator.clone(), coin(200u128, "ampWHALE"), |result| { + result.unwrap(); + }) + .fast_forward(1_000) + .unbond(creator.clone(), coin(300u128, "ampWHALE"), |result| { + result.unwrap(); + }) + .fast_forward(1_000) + .unbond(creator.clone(), coin(400u128, "ampWHALE"), |result| { + result.unwrap(); + }); + + suite + .query_unbonding( + creator.clone().to_string(), + "ampWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert_eq!( + unbonding_response, + UnbondingResponse { + total_amount: Uint128::new(1_000), + unbonding_requests: vec![ + Bond { + asset: coin(100, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572056619), + last_updated: 3, + weight: Default::default(), + }, + Bond { + asset: coin(200, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572057619), + last_updated: 3, + weight: Default::default(), + }, + Bond { + asset: coin(300, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572058619), + last_updated: 3, + weight: Default::default(), + }, + Bond { + asset: coin(400, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572059619), + last_updated: 3, + weight: Default::default(), + }, + ], + } + ); + }, + ) + .query_unbonding( + creator.clone().to_string(), + "ampWHALE".to_string(), + None, + Some(2), + |res| { + let unbonding_response = res.unwrap().1; + assert_eq!( + unbonding_response, + UnbondingResponse { + total_amount: Uint128::new(300), + unbonding_requests: vec![ + Bond { + asset: coin(100, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572056619), + last_updated: 3, + weight: Default::default(), + }, + Bond { + asset: coin(200, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572057619), + last_updated: 3, + weight: Default::default(), + }, + ], + } + ); + }, + ); + + suite.query_unbonding( + creator.clone().to_string(), + "ampWHALE".to_string(), + Some(1572057619), + Some(2), + |res| { + let unbonding_response = res.unwrap().1; + assert_eq!( + unbonding_response, + UnbondingResponse { + total_amount: Uint128::new(700), + unbonding_requests: vec![ + Bond { + asset: coin(300, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572058619), + last_updated: 3, + weight: Default::default(), + }, + Bond { + asset: coin(400, "ampWHALE"), + created_at_epoch: 3, + unbonded_at: Some(1572059619), + last_updated: 3, + weight: Default::default(), + }, + ], + } + ); + }, + ); + + // epoch 4 + suite.add_one_day().create_new_epoch(); + + suite + .query_global_index(Some(4u64), |result| { + let global_index = result.unwrap().1; + assert_eq!( + global_index, + GlobalIndex { + epoch_id: 4, + bonded_amount: Uint128::zero(), + bonded_assets: vec![], + last_updated: 4, + last_weight: Uint128::zero(), + } + ); + }) + .query_global_index(Some(5u64), |result| { + let global_index = result.unwrap().1; + assert_eq!(global_index, GlobalIndex::default()); + }) + .query_global_index(Some(100u64), |result| { + let global_index = result.unwrap().1; + assert_eq!(global_index, GlobalIndex::default()); + }); +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 4158ee63..f56f2912 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -1,9 +1,6 @@ use anyhow::Error; -use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{ - coin, from_json, Addr, Binary, Coin, Decimal, Empty, OwnedDeps, StdResult, Uint128, Uint64, -}; -// use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; +use cosmwasm_std::testing::{MockApi, MockStorage}; +use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Uint128, Uint64}; use cw_multi_test::{ App, AppBuilder, AppResponse, BankKeeper, DistributionKeeper, Executor, FailingModule, GovFailingModule, IbcFailingModule, StakeKeeper, WasmKeeper, @@ -11,11 +8,10 @@ use cw_multi_test::{ use white_whale_std::fee::PoolFee; use white_whale_testing::multi_test::stargate_mock::StargateMock; -use crate::state::{CONFIG, REWARD_BUCKETS}; use cw_multi_test::{Contract, ContractWrapper}; use white_whale_std::bonding_manager::{ - BondedResponse, BondingWeightResponse, Config, ExecuteMsg, GlobalIndex, InstantiateMsg, - QueryMsg, RewardsResponse, UnbondingResponse, WithdrawableResponse, + BondedResponse, Config, ExecuteMsg, GlobalIndex, InstantiateMsg, QueryMsg, RewardsResponse, + UnbondingResponse, WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; @@ -75,8 +71,6 @@ pub struct TestingSuite { pub bonding_manager_addr: Addr, pub pool_manager_addr: Addr, pub epoch_manager_addr: Addr, - owned_deps: OwnedDeps, - env: cosmwasm_std::Env, } /// instantiate / execute messages @@ -123,8 +117,6 @@ impl TestingSuite { bonding_manager_addr: Addr::unchecked(""), pool_manager_addr: Addr::unchecked(""), epoch_manager_addr: Addr::unchecked(""), - owned_deps: mock_dependencies(), - env: mock_env(), } } @@ -194,8 +186,7 @@ impl TestingSuite { white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::AddHook { contract_addr: bonding_manager_addr.clone().to_string(), }; - let resp = self - .app + self.app .execute_contract( self.senders[0].clone(), epoch_manager_addr.clone(), @@ -468,7 +459,7 @@ impl TestingSuite { #[track_caller] pub(crate) fn query_global_index( &mut self, - epoch_id: Option, + reward_bucket_id: Option, response: impl Fn(StdResult<(&mut Self, GlobalIndex)>), ) -> &mut Self { let global_index: GlobalIndex = self @@ -476,7 +467,7 @@ impl TestingSuite { .wrap() .query_wasm_smart( &self.bonding_manager_addr, - &QueryMsg::GlobalIndex { epoch_id }, + &QueryMsg::GlobalIndex { reward_bucket_id }, ) .unwrap(); diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs deleted file mode 100644 index 06584511..00000000 --- a/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs +++ /dev/null @@ -1,445 +0,0 @@ -use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128, Uint64}; - -use white_whale_std::bonding_manager::{ - Bond, BondedResponse, BondingWeightResponse, UnbondingResponse, -}; - -use crate::tests::suite::TestingSuite; - -#[test] -#[track_caller] -fn test_unbond_successfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - let another_sender = robot.another_sender.clone(); - - robot - .instantiate_default() - .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |res| { - println!("{:?}", res.unwrap()); - println!("Bonded successfully\n\n\n"); - }, - ) - .fast_forward(10u64) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(11_000u128), - global_weight: Uint128::new(11_000u128), - share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797429879305533u64), - }, - ) - .unbond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(300u128), - }, - |res| { - println!("{:?}", res.unwrap()); - }, - ) - .fast_forward(10u64) - .assert_unbonding_response( - sender.to_string(), - "ampWHALE".to_string(), - UnbondingResponse { - total_amount: Uint128::new(300u128), - unbonding_requests: vec![Bond { - asset: Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(300u128), - }, - timestamp: Timestamp::from_nanos(1571797429879305533u64), - weight: Uint128::zero(), - }], - }, - ) - .assert_unbonding_response( - sender.to_string(), - "bWHALE".to_string(), - UnbondingResponse { - total_amount: Uint128::zero(), - unbonding_requests: vec![], - }, - ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(700u128), - bonded_assets: vec![Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(700u128), - }], - first_bonded_epoch_id: Uint64::one(), - }, - ) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(14_700u128), - global_weight: Uint128::new(14_700u128), - share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797439879305533u64), - }, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(200u128), - }, - |_res| {}, - ) - .assert_unbonding_response( - sender.to_string(), - "ampWHALE".to_string(), - UnbondingResponse { - total_amount: Uint128::new(500u128), - unbonding_requests: vec![ - Bond { - asset: Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(300u128), - }, - timestamp: Timestamp::from_nanos(1571797429879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(200u128), - }, - timestamp: Timestamp::from_nanos(1571797449879305533u64), - weight: Uint128::zero(), - }, - ], - }, - ) - .bond( - another_sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "bWHALE"), - |_res| {}, - ) - .query_total_bonded(|res| { - let bonded_response = res.unwrap().1; - assert_eq!( - bonded_response, - BondedResponse { - total_bonded: Uint128::new(1_500u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(500u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - ], - first_bonded_epoch_id: Default::default(), - } - ) - }); -} - -#[test] -fn test_unbond_all_successfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - - robot - .instantiate_default() - .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(11_000u128), - global_weight: Uint128::new(11_000u128), - share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797429879305533u64), - }, - ) - .unbond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1000u128), - }, - |res| { - res.unwrap(); - }, - ); -} - -#[test] -#[track_caller] -fn test_unbonding_query_pagination() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - - robot - .instantiate_default() - .bond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - |_res| {}, - ) - .fast_forward(10u64) - .query_unbonding( - sender.to_string(), - "ampWHALE".to_string(), - None, - None, - |res| { - assert_eq!( - res.unwrap().1, - UnbondingResponse { - total_amount: Uint128::new(400u128), - unbonding_requests: vec![ - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797429879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797439879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797449879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797459879305533u64), - weight: Uint128::zero(), - }, - ], - } - ) - }, - ) - .query_unbonding( - sender.to_string(), - "ampWHALE".to_string(), - None, - Some(2u8), - |res| { - assert_eq!( - res.unwrap().1, - UnbondingResponse { - total_amount: Uint128::new(200u128), - unbonding_requests: vec![ - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797429879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797439879305533u64), - weight: Uint128::zero(), - }, - ], - } - ) - }, - ) - .query_unbonding( - sender.to_string(), - "ampWHALE".to_string(), - Some(12365u64), // start after the block height of the last item in the previous query - Some(2u8), - |res| { - assert_eq!( - res.unwrap().1, - UnbondingResponse { - total_amount: Uint128::new(200u128), - unbonding_requests: vec![ - Bond { - asset: Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797429879305533u64), - weight: Uint128::zero(), - }, - Bond { - asset: Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(100u128), - }, - timestamp: Timestamp::from_nanos(1571797439879305533u64), - weight: Uint128::zero(), - }, - ], - } - ) - }, - ); -} - -#[test] -fn test_unbond_unsuccessfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - - robot - .instantiate_default() - .bond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "wrong_token".to_string(), - amount: Uint128::new(1_000u128), - }, - |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is InvalidBondingAsset - }, - ) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToUnbond - }, - ) - .unbond( - sender.clone(), - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(2_000u128), - }, - |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is InsufficientBond - }, - ) - .unbond( - sender, - Coin { - // Change 'Asset' to 'Coin' - denom: "ampWHALE".to_string(), - amount: Uint128::new(0u128), - }, - |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is InvalidUnbondingAmount - }, - ); -} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs b/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs new file mode 100644 index 00000000..47ede32b --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs @@ -0,0 +1,1386 @@ +use std::cell::RefCell; + +use cosmwasm_std::{coin, coins, Coin, Decimal, Timestamp, Uint128}; + +use white_whale_std::bonding_manager::{ + Bond, BondedResponse, GlobalIndex, RewardBucket, UnbondingResponse, +}; +use white_whale_std::fee::{Fee, PoolFee}; +use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; + +use crate::tests::suite::TestingSuite; +use crate::ContractError; + +#[test] +fn test_unbonding_withdraw() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); + let another_sender = suite.senders[1].clone(); + let yet_another_sender = suite.senders[2].clone(); + + let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; + + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + suite + .instantiate_default() + .bond(creator.clone(), &coins(1_000u128, "ampWHALE"), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // the system has not been initialized + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }); + + suite + .fast_forward(259_200) + // epoch 1 + .create_new_epoch() + .create_pair( + creator.clone(), + asset_denoms.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uusdc".to_string(), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1_000_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .swap( + creator.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + // epoch 2 + .create_new_epoch() + .swap( + creator.clone(), + coin(2_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(2_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + // epoch 3 + suite.create_new_epoch(); + + // we bond tokens with the creator + suite + .bond(creator.clone(), &coins(1_000u128, "ampWHALE"), |res| { + res.unwrap(); + }) + .query_rewards(creator.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + // empty as the user just bonded + assert!(rewards.rewards.is_empty()); + }); + + // make some swaps to collect fees for the next epoch + suite + .swap( + creator.clone(), + coin(20_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(20_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(20_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(20_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + suite + .add_one_day() + // epoch 4 + .create_new_epoch(); + + // we bond more tokens with another user + + suite + .fast_forward(20_000) + .bond(another_sender.clone(), &coins(700u128, "bWHALE"), |res| { + res.unwrap(); + }) + .query_rewards(another_sender.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + // empty as the user just bonded + assert!(rewards.rewards.is_empty()); + }); + + // let's make some swaps to fill the next buckets and then compare the users' rewards + suite + .swap( + creator.clone(), + coin(100_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(100_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .add_one_day() + // epoch 5 + .create_new_epoch(); + + suite + .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(880u128)); + }) + .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(317u128)); + }); + + suite.bond( + yet_another_sender.clone(), + &coins(5_000u128, "bWHALE"), + |result| { + result.unwrap(); + }, + ); + + suite + .swap( + another_sender.clone(), + coin(80_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(80_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .add_one_day() + // epoch 6 + .create_new_epoch(); + + suite + .query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + + assert_eq!( + claimable_reward_buckets, + vec![ + RewardBucket { + id: 6, + epoch_start_time: Timestamp::from_nanos(1572315819879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 6, + bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + }, + ], + last_updated: 5, + last_weight: Uint128::new(9_400), + }, + }, + RewardBucket { + id: 5, + epoch_start_time: Timestamp::from_nanos(1572229419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 5, + bonded_amount: Uint128::new(1_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700), + }, + ], + last_updated: 4, + last_weight: Uint128::new(2_700), + }, + }, + RewardBucket { + id: 4, + epoch_start_time: Timestamp::from_nanos(1572143019879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(199), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(199), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 4, + bonded_amount: Uint128::new(1_000), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }], + last_updated: 3, + last_weight: Uint128::new(1_000), + }, + }, + RewardBucket { + id: 3, + epoch_start_time: Timestamp::from_nanos(1572056619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 3, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 3, + last_weight: Default::default(), + }, + }, + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + }, + }, + ] + ); + }) + .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)); + }) + .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)); + }) + .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)); + }); + + // let's unbond + + let creator_balance = RefCell::new(Uint128::zero()); + let another_sender_balance = RefCell::new(Uint128::zero()); + let yet_another_sender_balance = RefCell::new(Uint128::zero()); + + suite + .query_balance("uwhale".to_string(), creator.clone(), |balance| { + *creator_balance.borrow_mut() = balance; + }) + .query_balance("uwhale".to_string(), another_sender.clone(), |balance| { + *another_sender_balance.borrow_mut() = balance; + }) + .query_balance( + "uwhale".to_string(), + yet_another_sender.clone(), + |balance| { + *yet_another_sender_balance.borrow_mut() = balance; + }, + ); + + 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| { + let err = result.unwrap_err().downcast::().unwrap(); + // can't unbond an asset the user never bonded + match err { + ContractError::NothingToUnbond { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NothingToUnbond"), + } + }) + .unbond(creator.clone(), coin(100_000u128, "ampWHALE"), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // trying to unbond more than bonded + match err { + ContractError::InsufficientBond { .. } => {} + _ => panic!("Wrong error type, should return ContractError::InsufficientBond"), + } + }) + .unbond(creator.clone(), coin(0u128, "ampWHALE"), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // trying to unbond more than bonded + match err { + ContractError::InvalidUnbondingAmount { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::InvalidUnbondingAmount") + } + } + }) + .unbond(creator.clone(), coin(1_000u128, "ampWHALE"), |result| { + // total unbond + result.unwrap(); + }) + .query_bonded(Some(creator.clone().to_string()), |res| { + let bonded_response = res.unwrap().1; + assert_eq!(bonded_response.total_bonded, Uint128::zero()); + assert!(bonded_response.bonded_assets.is_empty()); + }) + .query_unbonding( + creator.clone().to_string(), + "bWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert!(unbonding_response.unbonding_requests.is_empty()); + }, + ) + .query_unbonding( + creator.clone().to_string(), + "ampWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert_eq!(unbonding_response.unbonding_requests.len(), 1); + assert_eq!( + unbonding_response.unbonding_requests[0], + Bond { + asset: coin(1_000u128, "ampWHALE"), + created_at_epoch: 6, + unbonded_at: Some(1572335819), + last_updated: 6, + weight: Uint128::zero(), + } + ); + }, + ) + .query_rewards(creator.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + assert!(rewards.rewards.is_empty()); + }) + .withdraw(creator.clone(), "bWHALE".to_string(), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // trying to withdraw something the user never unbonded + match err { + ContractError::NothingToWithdraw { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NothingToWithdraw"), + } + }) + .withdraw(creator.clone(), "ampWHALE".to_string(), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + // trying to withdraw before the unbonding period passed + match err { + ContractError::NothingToWithdraw { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NothingToWithdraw"), + } + }) + .query_withdrawable(creator.clone().to_string(), "ampWHALE".to_string(), |res| { + let withdrawable = res.unwrap().1; + assert_eq!(withdrawable.withdrawable_amount, Uint128::zero()); + }) + .add_one_day() + .create_new_epoch(); + + let creator_balance = RefCell::new(Uint128::zero()); + + suite + .query_balance("ampWHALE".to_string(), creator.clone(), |balance| { + *creator_balance.borrow_mut() = balance; + }) + .query_withdrawable(creator.clone().to_string(), "ampWHALE".to_string(), |res| { + let withdrawable = res.unwrap().1; + assert_eq!(withdrawable.withdrawable_amount, Uint128::new(1_000)); + }) + .withdraw(creator.clone(), "ampWHALE".to_string(), |result| { + result.unwrap(); + }) + .query_balance("ampWHALE".to_string(), creator.clone(), |balance| { + assert_eq!( + creator_balance.clone().into_inner() + Uint128::new(1000u128), + balance + ); + }) + .query_rewards(creator.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + assert!(rewards.rewards.is_empty()); + }); + + suite.query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + + assert_eq!( + claimable_reward_buckets, + vec![ + RewardBucket { + id: 6, + epoch_start_time: Timestamp::from_nanos(1572315819879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(601), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(198), + }], + global_index: GlobalIndex { + epoch_id: 6, + bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + }, + ], + last_updated: 5, + last_weight: Uint128::new(9_400), + }, + }, + RewardBucket { + id: 5, + epoch_start_time: Timestamp::from_nanos(1572229419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(318), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(681), + }], + global_index: GlobalIndex { + epoch_id: 5, + bonded_amount: Uint128::new(1_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700), + }, + ], + last_updated: 4, + last_weight: Uint128::new(2_700), + }, + }, + RewardBucket { + id: 3, + epoch_start_time: Timestamp::from_nanos(1572056619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 3, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 3, + last_weight: Default::default(), + }, + }, + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + }, + }, + ] + ); + }); + + suite + .swap( + another_sender.clone(), + coin(80_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(80_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .add_one_day() + .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"), + } + }) + .claim(another_sender.clone(), |result| { + result.unwrap(); + }) + .unbond(another_sender.clone(), coin(300u128, "bWHALE"), |result| { + // partial unbond + result.unwrap(); + }) + .fast_forward(1) + .unbond(another_sender.clone(), coin(200u128, "bWHALE"), |result| { + // partial unbond + result.unwrap(); + }) + .query_bonded(Some(another_sender.clone().to_string()), |res| { + let bonded_response = res.unwrap().1; + assert_eq!( + bonded_response, + BondedResponse { + total_bonded: Uint128::new(200), + bonded_assets: coins(200u128, "bWHALE"), + first_bonded_epoch_id: Some(4u64), + } + ); + }) + .query_unbonding( + another_sender.clone().to_string(), + "bWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert_eq!( + unbonding_response, + UnbondingResponse { + total_amount: Uint128::new(500), + unbonding_requests: vec![ + Bond { + asset: coin(300u128, "bWHALE"), + created_at_epoch: 8, + unbonded_at: Some(1572508619), + last_updated: 8, + weight: Uint128::zero(), + }, + Bond { + asset: coin(200u128, "bWHALE"), + created_at_epoch: 8, + unbonded_at: Some(1572508620), + last_updated: 8, + weight: Uint128::zero(), + } + ], + } + ); + }, + ); + + suite + .swap( + another_sender.clone(), + coin(80_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(80_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .add_one_day() + .create_new_epoch(); + + suite.query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + + assert_eq!( + claimable_reward_buckets, + vec![ + RewardBucket { + id: 9, + epoch_start_time: Timestamp::from_nanos(1572575019879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 9, + bonded_amount: Uint128::new(5_000 + 200), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 200), + },], + last_updated: 8, + last_weight: Uint128::new(21_001), + }, + }, + RewardBucket { + id: 8, + epoch_start_time: Timestamp::from_nanos(1572488619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(681), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(118), + }], + global_index: GlobalIndex { + epoch_id: 8, + bonded_amount: Uint128::new(5_000 + 700), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + },], + last_updated: 6, + last_weight: Uint128::new(12_100), + }, + }, + RewardBucket { + id: 6, + epoch_start_time: Timestamp::from_nanos(1572315819879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(497), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(302), + }], + global_index: GlobalIndex { + epoch_id: 6, + bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + }, + ], + last_updated: 5, + last_weight: Uint128::new(9_400), + }, + }, + RewardBucket { + id: 5, + epoch_start_time: Timestamp::from_nanos(1572229419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(998), + }], + global_index: GlobalIndex { + epoch_id: 5, + bonded_amount: Uint128::new(1_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700), + }, + ], + last_updated: 4, + last_weight: Uint128::new(2_700), + }, + }, + RewardBucket { + id: 3, + epoch_start_time: Timestamp::from_nanos(1572056619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 3, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 3, + last_weight: Default::default(), + }, + }, + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + }, + }, + ] + ); + }); + + suite + .withdraw(another_sender.clone(), "bWHALE".to_string(), |result| { + result.unwrap(); + }) + .query_unbonding( + another_sender.clone().to_string(), + "bWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert!(unbonding_response.unbonding_requests.is_empty()); + }, + ) + .query_bonded(Some(another_sender.clone().to_string()), |res| { + let bonded_response = res.unwrap().1; + assert_eq!( + bonded_response, + BondedResponse { + total_bonded: Uint128::new(200), + bonded_assets: coins(200u128, "bWHALE"), + first_bonded_epoch_id: Some(4u64), + } + ); + }) + .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| { + // total unbond + result.unwrap(); + }); + + suite + .swap( + another_sender.clone(), + coin(80_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(80_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .add_one_day() + .create_new_epoch(); + + suite + .withdraw(another_sender.clone(), "bWHALE".to_string(), |result| { + result.unwrap(); + }) + .query_unbonding( + another_sender.clone().to_string(), + "bWHALE".to_string(), + None, + None, + |res| { + let unbonding_response = res.unwrap().1; + assert!(unbonding_response.unbonding_requests.is_empty()); + }, + ) + .query_bonded(Some(another_sender.clone().to_string()), |res| { + let bonded_response = res.unwrap().1; + assert_eq!(bonded_response.total_bonded, Uint128::zero()); + assert!(bonded_response.bonded_assets.is_empty()); + }); + + suite.query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + + assert_eq!( + claimable_reward_buckets, + vec![ + RewardBucket { + id: 10, + epoch_start_time: Timestamp::from_nanos(1572661419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 10, + bonded_amount: Uint128::new(5_000), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000), + },], + last_updated: 9, + // now the yet_another_sender has 100% of the weight + last_weight: Uint128::new(25_000), + }, + }, + RewardBucket { + id: 9, + epoch_start_time: Timestamp::from_nanos(1572575019879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(763), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(36), + }], + global_index: GlobalIndex { + epoch_id: 9, + bonded_amount: Uint128::new(5_000 + 200), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 200), + },], + last_updated: 8, + last_weight: Uint128::new(21_001), + }, + }, + RewardBucket { + id: 8, + epoch_start_time: Timestamp::from_nanos(1572488619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(681), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(118), + }], + global_index: GlobalIndex { + epoch_id: 8, + bonded_amount: Uint128::new(5_000 + 700), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + },], + last_updated: 6, + last_weight: Uint128::new(12_100), + }, + }, + RewardBucket { + id: 6, + epoch_start_time: Timestamp::from_nanos(1572315819879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(497), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(302), + }], + global_index: GlobalIndex { + epoch_id: 6, + bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + }, + ], + last_updated: 5, + last_weight: Uint128::new(9_400), + }, + }, + RewardBucket { + id: 5, + epoch_start_time: Timestamp::from_nanos(1572229419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(998), + }], + global_index: GlobalIndex { + epoch_id: 5, + bonded_amount: Uint128::new(1_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700), + }, + ], + last_updated: 4, + last_weight: Uint128::new(2_700), + }, + }, + RewardBucket { + id: 3, + epoch_start_time: Timestamp::from_nanos(1572056619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 3, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 3, + last_weight: Default::default(), + }, + }, + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + }, + }, + ] + ); + }); + + suite + .claim(yet_another_sender.clone(), |result| { + result.unwrap(); + }) + .query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + // epoch 10 disappeared because it was totally claimed by yet_another_sender + assert_eq!( + claimable_reward_buckets, + vec![ + RewardBucket { + id: 9, + epoch_start_time: Timestamp::from_nanos(1572575019879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(798), + }], + global_index: GlobalIndex { + epoch_id: 9, + bonded_amount: Uint128::new(5_000 + 200), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 200), + },], + last_updated: 8, + last_weight: Uint128::new(21_001), + }, + }, + RewardBucket { + id: 8, + epoch_start_time: Timestamp::from_nanos(1572488619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(2), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(797), + }], + global_index: GlobalIndex { + epoch_id: 8, + bonded_amount: Uint128::new(5_000 + 700), + bonded_assets: vec![Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + },], + last_updated: 6, + last_weight: Uint128::new(12_100), + }, + }, + RewardBucket { + id: 6, + epoch_start_time: Timestamp::from_nanos(1572315819879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(799), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(798), + }], + global_index: GlobalIndex { + epoch_id: 6, + bonded_amount: Uint128::new(1_000 + 5_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000 + 700), + }, + ], + last_updated: 5, + last_weight: Uint128::new(9_400), + }, + }, + RewardBucket { + id: 5, + epoch_start_time: Timestamp::from_nanos(1572229419879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(999), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1), + }], + claimed: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(998), + }], + global_index: GlobalIndex { + epoch_id: 5, + bonded_amount: Uint128::new(1_000 + 700), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700), + }, + ], + last_updated: 4, + last_weight: Uint128::new(2_700), + }, + }, + RewardBucket { + id: 3, + epoch_start_time: Timestamp::from_nanos(1572056619879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(19), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 3, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 3, + last_weight: Default::default(), + }, + }, + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 2, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 2, + last_weight: Default::default(), + }, + }, + ] + ); + }); +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs b/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs deleted file mode 100644 index 0abf1d75..00000000 --- a/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs +++ /dev/null @@ -1,113 +0,0 @@ -use cosmwasm_std::{coins, Coin, Event, Uint128}; - -use white_whale_std::bonding_manager::WithdrawableResponse; - -use crate::tests::suite::TestingSuite; - -#[test] -fn test_withdraw_successfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - let another_sender = robot.another_sender.clone(); - - robot.instantiate_default(); - - let bonding_manager_addr = robot.bonding_manager_addr.clone(); - - robot - .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(300u128), - }, - |_res| {}, - ) - .fast_forward(1000u64) - .assert_withdrawable_response( - sender.clone().to_string(), - "ampWHALE".to_string(), - WithdrawableResponse { - withdrawable_amount: Uint128::new(300u128), - }, - ) - .assert_withdrawable_response( - another_sender.to_string(), - "ampWHALE".to_string(), - WithdrawableResponse { - withdrawable_amount: Uint128::zero(), - }, - ); - robot.withdraw(sender.clone(), "ampWHALE".to_string(), |res| { - let events = res.unwrap().events; - let transfer_event = events.last().unwrap().clone(); - assert_eq!( - transfer_event, - Event::new("transfer").add_attributes(vec![ - ("recipient", sender.to_string()), - ("sender", bonding_manager_addr.to_string()), - ("amount", "300ampWHALE".to_string()), - ]) - ); - }); -} - -#[test] -fn test_withdraw_unsuccessfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - let another_sender = robot.another_sender.clone(); - - robot - .instantiate_default() - .withdraw(sender.clone(), "ampWHALE".to_string(), |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToWithdraw - }) - .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .unbond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(300u128), - }, - |_res| {}, - ) - .withdraw(sender.clone(), "ampWHALE".to_string(), |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToWithdraw - }) - .fast_forward(999u64) //unbonding period is 1000 - .withdraw(sender.clone(), "ampWHALE".to_string(), |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToWithdraw - }) - .fast_forward(999u64) //unbonding period is 1000 - .withdraw(sender.clone(), "bWHALE".to_string(), |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToWithdraw - }) - .withdraw(another_sender, "ampWHALE".to_string(), |res| { - println!("{:?}", res.unwrap_err().root_cause()); - //assert error is NothingToWithdraw - }); -} diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index cf11b62e..ae9632df 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -54,15 +54,16 @@ pub struct UpcomingRewardBucket { #[cw_serde] pub struct Bond { - /// The amount of bonded tokens. - pub asset: Coin, /// The epoch id at which the Bond was created. pub created_at_epoch: u64, /// The epoch id at which the bond was last time updated. pub last_updated: u64, + /// The amount of bonded tokens. + pub asset: Coin, /// The weight of the bond at the given block height. pub weight: Uint128, - //pub previous: Option<(u64, Uint128)> + /// The time at which the Bond was unbonded. + pub unbonded_at: Option, } impl Default for Bond { @@ -73,9 +74,9 @@ impl Default for Bond { amount: Uint128::zero(), }, created_at_epoch: Default::default(), + unbonded_at: None, last_updated: Default::default(), weight: Uint128::zero(), - //previous: None, } } } @@ -166,7 +167,6 @@ pub enum QueryMsg { /// Returns the [Config] of te contract. #[returns(Config)] Config, - /// Returns the amount of assets that have been bonded by the specified address. #[returns(BondedResponse)] Bonded { @@ -174,7 +174,6 @@ pub enum QueryMsg { /// contract are returned. address: Option, }, - /// Returns the amount of tokens of the given denom that are been unbonded by the specified address. /// Allows pagination with start_after and limit. #[returns(UnbondingResponse)] @@ -188,7 +187,6 @@ pub enum QueryMsg { /// The maximum amount of unbonding assets to return. limit: Option, }, - /// Returns the amount of unbonding tokens of the given denom for the specified address that can /// be withdrawn, i.e. that have passed the unbonding period. #[returns(WithdrawableResponse)] @@ -198,29 +196,13 @@ pub enum QueryMsg { /// The denom to check for withdrawable assets. denom: String, }, - - //todo maybe this should be removed? No need to expose this if what's important is how many rewards - // the user have, which can be given with the Rewards query - /// Returns the weight of the address. - // #[returns(BondingWeightResponse)] - // Weight { - // /// The address to check for weight. - // address: String, - // /// The timestamp to check for weight. If none is provided, the current block time is used. - // epoch_id: Option, - // /// The global index to check for weight. If none is provided, the current global index is used. - // global_index: Option, - // }, - /// Returns the global index of the contract. #[returns(GlobalIndex)] GlobalIndex { - /// The epoch id to check for the global index. If none is provided, the current global index + /// The reward bucket id to check for the global index. If none is provided, the current global index /// is returned. - epoch_id: Option, + reward_bucket_id: Option, }, - - //todo maybe we don't need to expose this? /// Returns the [RewardBucket]s that can be claimed by an address. #[returns(ClaimableRewardBucketsResponse)] Claimable { @@ -228,8 +210,6 @@ pub enum QueryMsg { /// reward buckets stored in the contract that can potentially be claimed are returned. address: Option, }, - - //todo add rewards query that show how much a user can claim at that point of time /// Returns the rewards for the given address. #[returns(RewardsResponse)] Rewards { address: String }, diff --git a/packages/white-whale-std/src/coin.rs b/packages/white-whale-std/src/coin.rs index 3224fea1..3423c466 100644 --- a/packages/white-whale-std/src/coin.rs +++ b/packages/white-whale-std/src/coin.rs @@ -158,8 +158,7 @@ fn get_factory_token_label(denom: &str) -> StdResult { Ok(format!("{FACTORY_PREFIX}/{token_creator}/{token_subdenom}")) } -//todo test these functions in isolation -// move to ww package +/// Deducts the coins in `to_deduct` from `coins` if they exist. pub fn deduct_coins(coins: Vec, to_deduct: Vec) -> StdResult> { let mut updated_coins = coins.to_vec(); @@ -174,6 +173,8 @@ pub fn deduct_coins(coins: Vec, to_deduct: Vec) -> StdResult Uint128::zero()); + Ok(updated_coins) } /// Aggregates coins from two vectors, summing up the amounts of coins that are the same.