From 84ae06b74da6be958b36b4c7f89982d4ed462ad1 Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 6 May 2024 16:08:58 +0200 Subject: [PATCH 01/51] feat(smart-contracts): add stableswap support for provide liquidity --- .../liquidity_hub/pool-manager/src/helpers.rs | 145 ++++++++- .../pool-manager/src/liquidity/commands.rs | 80 ++++- .../liquidity_hub/pool-manager/src/state.rs | 9 + .../src/tests/integration_tests.rs | 308 +++++++++++++++++- 4 files changed, 515 insertions(+), 27 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 3e7870f6..319dfbd5 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -18,7 +18,7 @@ const NEWTON_ITERATIONS: u64 = 32; // todo isn't this for the 3pool? shouldn't it be 3 // the number of assets in the pool -const N_COINS: Uint256 = Uint256::from_u128(2); +pub const N_COINS: u8 = 3; fn calculate_stableswap_d( offer_pool: Decimal256, @@ -26,7 +26,7 @@ fn calculate_stableswap_d( amp: &u64, precision: u8, ) -> Result { - let n_coins = Decimal256::from_ratio(N_COINS, Uint256::from_u128(1)); + let n_coins = Decimal256::from_ratio(Uint256::from(N_COINS), Uint256::from_u128(1)); let sum_pools = offer_pool.checked_add(ask_pool)?; if sum_pools.is_zero() { @@ -35,7 +35,10 @@ fn calculate_stableswap_d( } // ann = amp * n_coins - let ann = Decimal256::from_ratio(Uint256::from_u128((*amp).into()).checked_mul(N_COINS)?, 1u8); + let ann = Decimal256::from_ratio( + Uint256::from_u128((*amp).into()).checked_mul(Uint256::from(N_COINS))?, + 1u8, + ); // perform Newton-Raphson method let mut current_d = sum_pools; @@ -99,7 +102,7 @@ pub fn calculate_stableswap_y( ask_precision: u8, direction: StableSwapDirection, ) -> Result { - let ann = Uint256::from_u128((*amp).into()).checked_mul(N_COINS)?; + let ann = Uint256::from_u128((*amp).into()).checked_mul(Uint256::from(N_COINS))?; let d = calculate_stableswap_d(offer_pool, ask_pool, amp, ask_precision)? .to_uint256_with_precision(u32::from(ask_precision))?; @@ -111,8 +114,8 @@ pub fn calculate_stableswap_y( .to_uint256_with_precision(u32::from(ask_precision))?; let c = d - .checked_multiply_ratio(d, pool_sum.checked_mul(N_COINS)?)? - .checked_multiply_ratio(d, ann.checked_mul(N_COINS)?)?; + .checked_multiply_ratio(d, pool_sum.checked_mul(Uint256::from(N_COINS))?)? + .checked_multiply_ratio(d, ann.checked_mul(Uint256::from(N_COINS))?)?; let b = pool_sum.checked_add(d.checked_div(ann)?)?; @@ -474,8 +477,8 @@ pub struct OfferAmountComputation { pub fn assert_slippage_tolerance( slippage_tolerance: &Option, - deposits: &[Uint128; 2], - pools: &[Coin; 2], + deposits: &Vec, + pools: &Vec, pool_type: PoolType, amount: Uint128, pool_token_supply: Uint128, @@ -487,7 +490,7 @@ pub fn assert_slippage_tolerance( } let one_minus_slippage_tolerance = Decimal256::one() - slippage_tolerance; - let deposits: [Uint256; 2] = [deposits[0].into(), deposits[1].into()]; + let deposits: [Uint256; 2] = [deposits[0].amount.into(), deposits[1].amount.into()]; let pools: [Uint256; 2] = [pools[0].amount.into(), pools[1].amount.into()]; // Ensure each prices are not dropped as much as slippage tolerance rate @@ -662,3 +665,127 @@ pub fn get_asset_indexes_in_pool( ask_decimal, )) } + +// TODO: handle unwraps properly +#[allow(clippy::unwrap_used)] +pub fn compute_d( + amp_factor: &u64, + amount_a: Uint128, + amount_b: Uint128, + amount_c: Uint128, +) -> Option { + let sum_x = amount_a + .checked_add(amount_b.checked_add(amount_c).unwrap()) + .unwrap(); // sum(x_i), a.k.a S + if sum_x == Uint128::zero() { + Some(Uint256::zero()) + } else { + let amount_a_times_coins = amount_a.checked_mul(N_COINS.into()).unwrap(); + let amount_b_times_coins = amount_b.checked_mul(N_COINS.into()).unwrap(); + let amount_c_times_coins = amount_c.checked_mul(N_COINS.into()).unwrap(); + + // Newton's method to approximate D + let mut d_prev: Uint256; + let mut d: Uint256 = sum_x.into(); + for _ in 0..256 { + let mut d_prod = d; + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount_a_times_coins.into()) + .unwrap(); + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount_b_times_coins.into()) + .unwrap(); + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount_c_times_coins.into()) + .unwrap(); + d_prev = d; + d = compute_next_d(amp_factor, d, d_prod, sum_x).unwrap(); + // Equality with the precision of 1 + if d > d_prev { + if d.checked_sub(d_prev).unwrap() <= Uint256::one() { + break; + } + } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { + break; + } + } + + Some(d) + } +} + +// TODO: handle unwraps properly +#[allow(clippy::unwrap_used)] +fn compute_next_d( + amp_factor: &u64, + d_init: Uint256, + d_prod: Uint256, + sum_x: Uint128, +) -> Option { + let ann = amp_factor.checked_mul(N_COINS.into())?; + let leverage = Uint256::from(sum_x).checked_mul(ann.into()).unwrap(); + // d = (ann * sum_x + d_prod * n_coins) * d / ((ann - 1) * d + (n_coins + 1) * d_prod) + let numerator = d_init + .checked_mul( + d_prod + .checked_mul(N_COINS.into()) + .unwrap() + .checked_add(leverage) + .unwrap(), + ) + .unwrap(); + let denominator = d_init + .checked_mul(ann.checked_sub(1)?.into()) + .unwrap() + .checked_add( + d_prod + .checked_mul((N_COINS.checked_add(1)?).into()) + .unwrap(), + ) + .unwrap(); + Some(numerator.checked_div(denominator).unwrap()) +} + +/// Computes the amount of pool tokens to mint after a deposit. +#[allow(clippy::unwrap_used, clippy::too_many_arguments)] +pub fn compute_mint_amount_for_deposit( + amp_factor: &u64, + deposit_amount_a: Uint128, + deposit_amount_b: Uint128, + deposit_amount_c: Uint128, + swap_amount_a: Uint128, + swap_amount_b: Uint128, + swap_amount_c: Uint128, + pool_token_supply: Uint128, +) -> Option { + // Initial invariant + let d_0 = compute_d(amp_factor, swap_amount_a, swap_amount_b, swap_amount_c)?; + let new_balances = [ + swap_amount_a.checked_add(deposit_amount_a).unwrap(), + swap_amount_b.checked_add(deposit_amount_b).unwrap(), + swap_amount_c.checked_add(deposit_amount_c).unwrap(), + ]; + // Invariant after change + let d_1 = compute_d( + amp_factor, + new_balances[0], + new_balances[1], + new_balances[2], + )?; + if d_1 <= d_0 { + None + } else { + let amount = Uint256::from(pool_token_supply) + .checked_mul(d_1.checked_sub(d_0).unwrap()) + .unwrap() + .checked_div(d_0) + .unwrap(); + Some(Uint128::try_from(amount).unwrap()) + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 8d163a13..c17fbad1 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ coin, coins, ensure, to_json_binary, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, - MessageInfo, Response, StdError, SubMsg, + MessageInfo, Response, SubMsg, }; use cosmwasm_std::{Decimal, OverflowError, Uint128}; @@ -23,7 +23,7 @@ use crate::{ // After writing create_pool I see this can get quite verbose so attempting to // break it down into smaller modules which house some things like swap, liquidity etc use crate::contract::SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID; -use crate::helpers::aggregate_outgoing_fees; +use crate::helpers::{aggregate_outgoing_fees, compute_d, compute_mint_amount_for_deposit}; use crate::queries::query_simulation; use crate::state::{ LiquidityProvisionData, SingleSideLiquidityProvisionBuffer, @@ -251,22 +251,11 @@ pub fn provide_liquidity( .multiply_ratio(total_share, pool_assets[1].amount), ); - let deposits_as: [Uint128; 2] = deposits - .iter() - .map(|coin| coin.amount) - .collect::>() - .try_into() - .map_err(|_| StdError::generic_err("Error converting vector to array"))?; - let pools_as: [Coin; 2] = pool_assets - .to_vec() - .try_into() - .map_err(|_| StdError::generic_err("Error converting vector to array"))?; - // assert slippage tolerance helpers::assert_slippage_tolerance( &slippage_tolerance, - &deposits_as, - &pools_as, + &deposits, + &pool_assets, pool.pool_type.clone(), amount, total_share, @@ -275,10 +264,67 @@ pub fn provide_liquidity( amount } } - PoolType::StableSwap { amp: _ } => { + PoolType::StableSwap { amp: amp_factor } => { // TODO: Handle stableswap - Uint128::one() + let share = if total_share == Uint128::zero() { + // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first + // depositor preventing small liquidity providers from joining the pool + let min_lp_token_amount = MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u8); + let share = Uint128::try_from( + compute_d( + amp_factor, + deposits[0].amount, + deposits[1].amount, + deposits[2].amount, + ) + .unwrap(), + )? + .checked_sub(min_lp_token_amount) + .map_err(|_| { + ContractError::InvalidInitialLiquidityAmount(min_lp_token_amount) + })?; + + // TODO: is this needed? I see it below after locking logic + // messages.append(&mut mint_lp_token_msg( + // liquidity_token.clone(), + // env.contract.address.to_string(), + // env.contract.address.to_string(), + // min_lp_token_amount, + // )?); + + // share should be above zero after subtracting the min_lp_token_amount + if share.is_zero() { + return Err(ContractError::InvalidInitialLiquidityAmount( + min_lp_token_amount, + )); + } + + share + } else { + let amount = compute_mint_amount_for_deposit( + amp_factor, + deposits[0].amount, + deposits[1].amount, + deposits[2].amount, + pool_assets[0].amount, + pool_assets[1].amount, + pool_assets[2].amount, + total_share, + ) + .unwrap(); + // assert slippage tolerance + helpers::assert_slippage_tolerance( + &slippage_tolerance, + &deposits, + &pool_assets, + pool.pool_type.clone(), + amount, + total_share, + )?; + amount + }; + share } }; diff --git a/contracts/liquidity_hub/pool-manager/src/state.rs b/contracts/liquidity_hub/pool-manager/src/state.rs index 7422f803..529fd04c 100644 --- a/contracts/liquidity_hub/pool-manager/src/state.rs +++ b/contracts/liquidity_hub/pool-manager/src/state.rs @@ -95,3 +95,12 @@ pub const SWAP_ROUTES: Map<(&str, &str), SwapOperations> = Map::new("swap_routes pub const CONFIG: Item = Item::new("config"); pub const POOL_COUNTER: Item = Item::new("pool_count"); + +pub const STABLE_SWAP_PARAMS: Item = Item::new("stable_swap_params"); +#[cw_serde] +pub struct StableSwapParams { + pub initial_amp: u64, + pub future_amp: u64, + pub initial_amp_block: u64, + pub future_amp_block: u64, +} diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index 80c1660d..8f5f02ea 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -2883,7 +2883,9 @@ mod locking_lp { } mod provide_liquidity { - use cosmwasm_std::{coin, Coin, Decimal, StdError, Uint128}; + use std::cell::RefCell; + + use cosmwasm_std::{assert_approx_eq, coin, Coin, Decimal, StdError, Uint128}; use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_manager::PoolType; @@ -3487,6 +3489,310 @@ mod provide_liquidity { }, ); } + + #[test] + fn provide_liquidity_stable_swap() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let asset_infos = vec![ + "uwhale".to_string(), + "uluna".to_string(), + "uusd".to_string(), + ]; + + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 10_000_u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_00u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + extra_fees: vec![], + }; + + // Create a pool + suite.instantiate_default().create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8, 6u8], + pool_fees, + PoolType::StableSwap { amp: 100 }, + Some("whale-uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount + for event in result.unwrap().events { + println!("{:?}", event); + } + }, + ); + let simulated_return_amount = RefCell::new(Uint128::zero()); + suite.query_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }, + "uluna".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; + }, + ); + + // Now lets try a swap + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + // Because the Pool was created and 1_000_000 of each token has been provided as liquidity + // Assuming no fees we should expect a small swap of 1000 to result in not too much slippage + // Expect 1000 give or take 0.002 difference + // Once fees are added and being deducted properly only the "0.002" should be changed. + assert_approx_eq!( + offer_amount.parse::().unwrap(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + }, + ); + + let simulated_offer_amount = RefCell::new(Uint128::zero()); + // Now lets try a reverse simulation by swapping uluna to uwhale + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000u128), + }, + "uluna".to_string(), + |result| { + *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + + // Another swap but this time the other way around + suite.swap( + creator.clone(), + "uwhale".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_offer_amount.borrow().u128(), + "uluna".to_string(), + )], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_offer_amount.borrow().u128(), + offer_amount.parse::().unwrap(), + "0.002" + ); + + assert_approx_eq!(1000u128, return_amount.parse::().unwrap(), "0.003"); + }, + ); + + // And now uwhale to uusd + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1000u128), + }, + "uwhale".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + // Another swap but this time uwhale to uusd + suite.swap( + creator.clone(), + "uusd".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_return_amount.borrow().u128(), + "uwhale".to_string(), + )], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!(1000u128, offer_amount.parse::().unwrap(), "0.003"); + }, + ); + + // And now uusd to uluna + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000u128), + }, + "uusd".to_string(), + |result| { + *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + // Another swap but this time uusd to uluna + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_offer_amount.borrow().u128(), + "uusd".to_string(), + )], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_offer_amount.borrow().u128(), + offer_amount.parse::().unwrap(), + "0.002" + ); + + assert_approx_eq!(1000u128, return_amount.parse::().unwrap(), "0.003"); + }, + ); + } } mod multiple_pools { From 4cb71f55191701ae1da2588399d3d1250bd7081f Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 6 May 2024 16:18:47 +0200 Subject: [PATCH 02/51] chore: please the lord clippy --- contracts/liquidity_hub/pool-manager/src/helpers.rs | 4 ++-- .../liquidity_hub/pool-manager/src/liquidity/commands.rs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 319dfbd5..65cf3f65 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -477,8 +477,8 @@ pub struct OfferAmountComputation { pub fn assert_slippage_tolerance( slippage_tolerance: &Option, - deposits: &Vec, - pools: &Vec, + deposits: &[Coin], + pools: &[Coin], pool_type: PoolType, amount: Uint128, pool_token_supply: Uint128, diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index c17fbad1..bd966f7e 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -267,7 +267,7 @@ pub fn provide_liquidity( PoolType::StableSwap { amp: amp_factor } => { // TODO: Handle stableswap - let share = if total_share == Uint128::zero() { + if total_share == Uint128::zero() { // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool let min_lp_token_amount = MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u8); @@ -323,8 +323,7 @@ pub fn provide_liquidity( total_share, )?; amount - }; - share + } } }; From c14fb19710cecede116a5c47e3d258b40baa04cd Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 7 May 2024 14:56:56 +0200 Subject: [PATCH 03/51] feat(smart-contracts): make provide liquidity for stableswap generic so we can provide liquidity with 2, 3, or N assets --- .../liquidity_hub/pool-manager/src/helpers.rs | 103 +++++---- .../pool-manager/src/liquidity/commands.rs | 34 +-- .../src/tests/integration_tests.rs | 196 +++++++++++++++++- 3 files changed, 249 insertions(+), 84 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 65cf3f65..4438b5e0 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -668,54 +668,47 @@ pub fn get_asset_indexes_in_pool( // TODO: handle unwraps properly #[allow(clippy::unwrap_used)] -pub fn compute_d( - amp_factor: &u64, - amount_a: Uint128, - amount_b: Uint128, - amount_c: Uint128, -) -> Option { - let sum_x = amount_a - .checked_add(amount_b.checked_add(amount_c).unwrap()) - .unwrap(); // sum(x_i), a.k.a S +pub fn compute_d(amp_factor: &u64, deposits: &Vec) -> Option { + let n_coins = Uint128::from(deposits.len() as u128); + + // sum(x_i), a.k.a S + let sum_x = deposits + .iter() + .fold(Uint128::zero(), |acc, x| acc.checked_add(x.amount).unwrap()) + .clone(); + if sum_x == Uint128::zero() { Some(Uint256::zero()) } else { - let amount_a_times_coins = amount_a.checked_mul(N_COINS.into()).unwrap(); - let amount_b_times_coins = amount_b.checked_mul(N_COINS.into()).unwrap(); - let amount_c_times_coins = amount_c.checked_mul(N_COINS.into()).unwrap(); + // do as below but for a generic number of assets + let amount_times_coins: Vec = deposits + .into_iter() + .map(|coin| coin.amount.checked_mul(n_coins).unwrap()) + .collect(); // Newton's method to approximate D let mut d_prev: Uint256; let mut d: Uint256 = sum_x.into(); - for _ in 0..256 { - let mut d_prod = d; - d_prod = d_prod - .checked_mul(d) - .unwrap() - .checked_div(amount_a_times_coins.into()) - .unwrap(); - d_prod = d_prod - .checked_mul(d) - .unwrap() - .checked_div(amount_b_times_coins.into()) - .unwrap(); - d_prod = d_prod - .checked_mul(d) - .unwrap() - .checked_div(amount_c_times_coins.into()) - .unwrap(); - d_prev = d; - d = compute_next_d(amp_factor, d, d_prod, sum_x).unwrap(); - // Equality with the precision of 1 - if d > d_prev { - if d.checked_sub(d_prev).unwrap() <= Uint256::one() { + for amount in amount_times_coins.into_iter() { + for _ in 0..256 { + let mut d_prod = d; + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount.into()) + .unwrap(); + d_prev = d; + d = compute_next_d(amp_factor, d, d_prod, sum_x).unwrap(); + // Equality with the precision of 1 + if d > d_prev { + if d.checked_sub(d_prev).unwrap() <= Uint256::one() { + break; + } + } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { break; } - } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { - break; } } - Some(d) } } @@ -756,28 +749,28 @@ fn compute_next_d( #[allow(clippy::unwrap_used, clippy::too_many_arguments)] pub fn compute_mint_amount_for_deposit( amp_factor: &u64, - deposit_amount_a: Uint128, - deposit_amount_b: Uint128, - deposit_amount_c: Uint128, - swap_amount_a: Uint128, - swap_amount_b: Uint128, - swap_amount_c: Uint128, + deposits: &Vec, + pool_assets: &Vec, pool_token_supply: Uint128, ) -> Option { // Initial invariant - let d_0 = compute_d(amp_factor, swap_amount_a, swap_amount_b, swap_amount_c)?; - let new_balances = [ - swap_amount_a.checked_add(deposit_amount_a).unwrap(), - swap_amount_b.checked_add(deposit_amount_b).unwrap(), - swap_amount_c.checked_add(deposit_amount_c).unwrap(), - ]; + let d_0 = compute_d(amp_factor, deposits)?; + + let new_balances: Vec = pool_assets + .iter() + .enumerate() + .map(|(i, pool_asset)| { + let deposit_amount = deposits[i].amount; + let new_amount = pool_asset.amount.checked_add(deposit_amount).unwrap(); + Coin { + denom: pool_asset.denom.clone(), + amount: new_amount, + } + }) + .collect(); + // Invariant after change - let d_1 = compute_d( - amp_factor, - new_balances[0], - new_balances[1], - new_balances[2], - )?; + let d_1 = compute_d(amp_factor, &new_balances)?; if d_1 <= d_0 { None } else { diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index bd966f7e..90910583 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -271,27 +271,11 @@ pub fn provide_liquidity( // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool let min_lp_token_amount = MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u8); - let share = Uint128::try_from( - compute_d( - amp_factor, - deposits[0].amount, - deposits[1].amount, - deposits[2].amount, - ) - .unwrap(), - )? - .checked_sub(min_lp_token_amount) - .map_err(|_| { - ContractError::InvalidInitialLiquidityAmount(min_lp_token_amount) - })?; - - // TODO: is this needed? I see it below after locking logic - // messages.append(&mut mint_lp_token_msg( - // liquidity_token.clone(), - // env.contract.address.to_string(), - // env.contract.address.to_string(), - // min_lp_token_amount, - // )?); + let share = Uint128::try_from(compute_d(amp_factor, &deposits).unwrap())? + .checked_sub(min_lp_token_amount) + .map_err(|_| { + ContractError::InvalidInitialLiquidityAmount(min_lp_token_amount) + })?; // share should be above zero after subtracting the min_lp_token_amount if share.is_zero() { @@ -304,12 +288,8 @@ pub fn provide_liquidity( } else { let amount = compute_mint_amount_for_deposit( amp_factor, - deposits[0].amount, - deposits[1].amount, - deposits[2].amount, - pool_assets[0].amount, - pool_assets[1].amount, - pool_assets[2].amount, + &deposits, + &pool_assets, total_share, ) .unwrap(); diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index 8f5f02ea..f535ddcc 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -2023,6 +2023,8 @@ mod swapping { ); } + // TODO: make sure stableswap test works when pool has ONLY 2 assets + #[test] fn basic_swapping_test_stable_swap() { let mut suite = TestingSuite::default_with_balances(vec![ @@ -3767,8 +3769,6 @@ mod provide_liquidity { "uusd".to_string(), )], |result| { - // Find the key with 'offer_amount' and the key with 'return_amount' - // Ensure that the offer amount is 1000 and the return amount is greater than 0 let mut return_amount = String::new(); let mut offer_amount = String::new(); @@ -3793,6 +3793,198 @@ mod provide_liquidity { }, ); } + + // This test is to ensure that the edge case of providing liquidity with 3 assets + #[test] + fn provide_liquidity_stable_swap_edge_case() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let asset_infos = vec![ + "uwhale".to_string(), + "uluna".to_string(), + "uusd".to_string(), + ]; + + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 10_000_u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_00u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + extra_fees: vec![], + }; + + // Create a pool with 3 assets + suite.instantiate_default().create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8, 6u8], + pool_fees, + PoolType::StableSwap { amp: 100 }, + Some("whale-uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Adding liquidity with less than the minimum liquidity amount should fail + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + }, + ], + |result| { + assert_eq!( + result.unwrap_err().downcast_ref::(), + Some(&ContractError::InvalidInitialLiquidityAmount( + MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u128) + )) + ); + }, + ); + + // Lets try to add liquidity with the correct amount (1_000_000 of each asset) + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount + for event in result.unwrap().events { + for attribute in event.attributes { + if attribute.key == "share" { + assert_approx_eq!( + attribute.value.parse::().unwrap(), + 1_000_000u128 * 3, + "0.002" + ); + } + } + } + }, + ); + + let simulated_return_amount = RefCell::new(Uint128::zero()); + suite.query_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }, + "uluna".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; + }, + ); + + // Now lets try a swap + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + // Because the Pool was created and 1_000_000 of each token has been provided as liquidity + // Assuming no fees we should expect a small swap of 1000 to result in not too much slippage + // Expect 1000 give or take 0.002 difference + // Once fees are added and being deducted properly only the "0.002" should be changed. + assert_approx_eq!( + offer_amount.parse::().unwrap(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + }, + ); + } } mod multiple_pools { From 0176c1ac5cc4936c9daff063a53846c70f6149e2 Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 7 May 2024 15:06:57 +0200 Subject: [PATCH 04/51] chore(smart-contracts): please the lord clippy --- contracts/liquidity_hub/pool-manager/src/helpers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 4438b5e0..f87de2b0 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -674,15 +674,14 @@ pub fn compute_d(amp_factor: &u64, deposits: &Vec) -> Option { // sum(x_i), a.k.a S let sum_x = deposits .iter() - .fold(Uint128::zero(), |acc, x| acc.checked_add(x.amount).unwrap()) - .clone(); + .fold(Uint128::zero(), |acc, x| acc.checked_add(x.amount).unwrap()); if sum_x == Uint128::zero() { Some(Uint256::zero()) } else { // do as below but for a generic number of assets let amount_times_coins: Vec = deposits - .into_iter() + .iter() .map(|coin| coin.amount.checked_mul(n_coins).unwrap()) .collect(); @@ -750,7 +749,7 @@ fn compute_next_d( pub fn compute_mint_amount_for_deposit( amp_factor: &u64, deposits: &Vec, - pool_assets: &Vec, + pool_assets: &[Coin], pool_token_supply: Uint128, ) -> Option { // Initial invariant From 9b00cc28d613bac1449403cdd798ed7d77cf8055 Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 7 May 2024 16:40:03 +0200 Subject: [PATCH 05/51] fix(smart-contracts): refactor to use generic n_coins using deposits.len() --- contracts/liquidity_hub/pool-manager/src/helpers.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index f87de2b0..c3135baf 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -697,7 +697,7 @@ pub fn compute_d(amp_factor: &u64, deposits: &Vec) -> Option { .checked_div(amount.into()) .unwrap(); d_prev = d; - d = compute_next_d(amp_factor, d, d_prod, sum_x).unwrap(); + d = compute_next_d(amp_factor, d, d_prod, sum_x, n_coins).unwrap(); // Equality with the precision of 1 if d > d_prev { if d.checked_sub(d_prev).unwrap() <= Uint256::one() { @@ -719,14 +719,15 @@ fn compute_next_d( d_init: Uint256, d_prod: Uint256, sum_x: Uint128, + n_coins: Uint128, ) -> Option { - let ann = amp_factor.checked_mul(N_COINS.into())?; + let ann = amp_factor.checked_mul(n_coins.u128() as u64)?; let leverage = Uint256::from(sum_x).checked_mul(ann.into()).unwrap(); // d = (ann * sum_x + d_prod * n_coins) * d / ((ann - 1) * d + (n_coins + 1) * d_prod) let numerator = d_init .checked_mul( d_prod - .checked_mul(N_COINS.into()) + .checked_mul(n_coins.into()) .unwrap() .checked_add(leverage) .unwrap(), @@ -737,7 +738,7 @@ fn compute_next_d( .unwrap() .checked_add( d_prod - .checked_mul((N_COINS.checked_add(1)?).into()) + .checked_mul((n_coins.checked_add(1u128.into()).unwrap()).into()) .unwrap(), ) .unwrap(); From 9fe2631e54259e7ba86ee08dcff48e506f34dd49 Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 7 May 2024 16:54:25 +0200 Subject: [PATCH 06/51] chore(smart-contracts): remvoe unnecessary comments --- contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 90910583..4ac77377 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -265,8 +265,6 @@ pub fn provide_liquidity( } } PoolType::StableSwap { amp: amp_factor } => { - // TODO: Handle stableswap - if total_share == Uint128::zero() { // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool @@ -293,7 +291,6 @@ pub fn provide_liquidity( total_share, ) .unwrap(); - // assert slippage tolerance helpers::assert_slippage_tolerance( &slippage_tolerance, &deposits, From 7f3d4e58f2d67eec295365ef2c7f9c32336a2c68 Mon Sep 17 00:00:00 2001 From: nahem Date: Thu, 23 May 2024 12:51:35 +0200 Subject: [PATCH 07/51] test(smart-contracts): add tests (including proptests) for pool manager with N coins --- Cargo.lock | 12 +- .../liquidity_hub/pool-manager/Cargo.toml | 3 + .../proptest-regressions/helpers.txt | 7 + .../liquidity_hub/pool-manager/sim/Cargo.toml | 12 + .../pool-manager/sim/simulation.py | 172 ++++++ .../liquidity_hub/pool-manager/sim/src/lib.rs | 209 +++++++ .../liquidity_hub/pool-manager/src/helpers.rs | 579 +++++++++++++++++- .../pool-manager/src/liquidity/commands.rs | 3 +- .../liquidity_hub/pool-manager/src/queries.rs | 4 +- .../pool-manager/src/swap/perform_swap.rs | 3 +- .../pool-network/stableswap_3pool/Cargo.toml | 2 +- .../stableswap_3pool/sim/Cargo.toml | 2 +- 12 files changed, 974 insertions(+), 34 deletions(-) create mode 100644 contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt create mode 100644 contracts/liquidity_hub/pool-manager/sim/Cargo.toml create mode 100644 contracts/liquidity_hub/pool-manager/sim/simulation.py create mode 100644 contracts/liquidity_hub/pool-manager/sim/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f99f8b59..bc444d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,9 +963,12 @@ dependencies = [ "cw20-base", "epoch-manager", "incentive-manager", + "proptest", + "rand", "semver", "serde", "sha2 0.10.8", + "stable-swap-sim", "test-case", "thiserror", "whale-lair", @@ -1418,6 +1421,13 @@ dependencies = [ "pyo3", ] +[[package]] +name = "stable-swap-sim1" +version = "0.1.0" +dependencies = [ + "pyo3", +] + [[package]] name = "stableswap-3pool" version = "1.2.3" @@ -1435,7 +1445,7 @@ dependencies = [ "schemars", "semver", "serde", - "stable-swap-sim", + "stable-swap-sim1", "thiserror", "white-whale-std", ] diff --git a/contracts/liquidity_hub/pool-manager/Cargo.toml b/contracts/liquidity_hub/pool-manager/Cargo.toml index 81032eca..f5dd6140 100644 --- a/contracts/liquidity_hub/pool-manager/Cargo.toml +++ b/contracts/liquidity_hub/pool-manager/Cargo.toml @@ -57,3 +57,6 @@ whale-lair.workspace = true incentive-manager.workspace = true epoch-manager.workspace = true white-whale-testing.workspace = true +proptest = "1.0.0" +rand = "0.8.4" +stable-swap-sim = { path = "./sim", version = "^0.1" } diff --git a/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt new file mode 100644 index 00000000..f09de62a --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 55857276de2241e3d09d36aba47854e0017db66f6c5a61e306b38ad0d3b8aeeb # shrinks to amp_factor = 1, initial_user_token_a_amount = 10000000, initial_user_token_b_amount = 10000000 diff --git a/contracts/liquidity_hub/pool-manager/sim/Cargo.toml b/contracts/liquidity_hub/pool-manager/sim/Cargo.toml new file mode 100644 index 00000000..e01fbc98 --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stable-swap-sim" +version = "0.1.0" +description = "Simulations of the StableSwap invariant compared to Curve's reference implementation." +authors = ["Paul Stelzig paul@irulast.com>"] +edition = "2021" + +[lib] +name = "sim" + +[dependencies] +pyo3 = { version = "0.17.3", features = ["auto-initialize"] } diff --git a/contracts/liquidity_hub/pool-manager/sim/simulation.py b/contracts/liquidity_hub/pool-manager/sim/simulation.py new file mode 100644 index 00000000..12606d3b --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/simulation.py @@ -0,0 +1,172 @@ +# Source from: https://github.com/curvefi/curve-contract/blob/master/tests/simulation.py + +class Curve: + + """ + Python model of Curve pool math. + """ + + def __init__(self, A, D, n, p=None, tokens=None): + """ + A: Amplification coefficient + D: Total deposit size + n: number of currencies + p: target prices + """ + self.A = A # actually A * n ** (n - 1) because it's an invariant + self.n = n + self.fee = 10 ** 7 + if p: + self.p = p + else: + self.p = [10 ** 18] * n + if isinstance(D, list): + self.x = D + else: + self.x = [D // n * 10 ** 18 // _p for _p in self.p] + self.tokens = tokens + + def xp(self): + return [x * p // 10 ** 18 for x, p in zip(self.x, self.p)] + + def D(self): + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + Dprev = 0 + xp = self.xp() + S = sum(xp) + D = S + Ann = self.A * self.n + while abs(D - Dprev) > 1: + D_P = D + for x in xp: + D_P = D_P * D // (self.n * x) + Dprev = D + D = (Ann * S + D_P * self.n) * D // ((Ann - 1) * D + (self.n + 1) * D_P) + + return D + + def y(self, i, j, x): + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + D = self.D() + xx = self.xp() + xx[i] = x # x is quantity of underlying asset brought to 1e18 precision + xx = [xx[k] for k in range(self.n) if k != j] + Ann = self.A * self.n + c = D + for y in xx: + c = c * D // (y * self.n) + c = c * D // (self.n * Ann) + b = sum(xx) + D // Ann - D + y_prev = 0 + y = D + while abs(y - y_prev) > 1: + y_prev = y + y = (y ** 2 + c) // (2 * y + b) + return y # the result is in underlying units too + + def y_D(self, i, _D): + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + xx = self.xp() + xx = [xx[k] for k in range(self.n) if k != i] + S = sum(xx) + Ann = self.A * self.n + c = _D + for y in xx: + c = c * _D // (y * self.n) + c = c * _D // (self.n * Ann) + b = S + _D // Ann + y_prev = 0 + y = _D + while abs(y - y_prev) > 1: + y_prev = y + y = (y ** 2 + c) // (2 * y + b - _D) + return y # the result is in underlying units too + + def dy(self, i, j, dx): + # dx and dy are in underlying units + xp = self.xp() + return xp[j] - self.y(i, j, xp[i] + dx) + + def exchange(self, i, j, dx): + xp = self.xp() + x = xp[i] + dx + y = self.y(i, j, x) + dy = xp[j] - y + fee = dy * self.fee // 10 ** 10 + assert dy > 0 + self.x[i] = x * 10 ** 18 // self.p[i] + self.x[j] = (y + fee) * 10 ** 18 // self.p[j] + return dy - fee + + def remove_liquidity_imbalance(self, amounts): + _fee = self.fee * self.n // (4 * (self.n - 1)) + + old_balances = self.x + new_balances = self.x[:] + D0 = self.D() + for i in range(self.n): + new_balances[i] -= amounts[i] + self.x = new_balances + D1 = self.D() + self.x = old_balances + fees = [0] * self.n + for i in range(self.n): + ideal_balance = D1 * old_balances[i] // D0 + difference = abs(ideal_balance - new_balances[i]) + fees[i] = _fee * difference // 10 ** 10 + new_balances[i] -= fees[i] + self.x = new_balances + D2 = self.D() + self.x = old_balances + + token_amount = (D0 - D2) * self.tokens // D0 + + return token_amount + + def calc_withdraw_one_coin(self, token_amount, i): + xp = self.xp() + xp_reduced = list(xp) + + D0 = self.D() + D1 = D0 - token_amount * D0 // self.tokens + new_y = self.y_D(i, D1) + + fee = self.fee * self.n // (4 * (self.n - 1)) + for j in range(self.n): + dx_expected = 0 + if j == i: + dx_expected = xp[j] * D1 // D0 - new_y + else: + dx_expected = xp[j] - xp[j] * D1 // D0 + xp_reduced[j] -= fee * dx_expected // 10 ** 10 + + self.x = [x * 10 ** 18 // p for x, p in zip(xp_reduced, self.p)] + dy = xp_reduced[i] - self.y_D(i, D1) - 1 # Withdraw less to account for rounding errors + self.x = [x * 10 ** 18 // p for x, p in zip(xp, self.p)] + dy_0 = xp[i] - new_y + + return dy, dy_0 - dy diff --git a/contracts/liquidity_hub/pool-manager/sim/src/lib.rs b/contracts/liquidity_hub/pool-manager/sim/src/lib.rs new file mode 100644 index 00000000..a9aef2dc --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/src/lib.rs @@ -0,0 +1,209 @@ +//! Simulations of the StableSwap invariant compared to Curve's reference implementation. +#![allow(deprecated)] +#![allow(deprecated)] + +use pyo3::prelude::*; +use pyo3::types::PyTuple; +use std::fs::File; +use std::io::prelude::*; + +pub const MODEL_FEE_NUMERATOR: u64 = 10000000; +pub const MODEL_FEE_DENOMINATOR: u64 = 10000000000; + +const DEFAULT_POOL_TOKENS: u128 = 0; +const DEFAULT_TARGET_PRICE: u128 = 1000000000000000000; +const FILE_NAME: &str = "simulation.py"; +const FILE_PATH: &str = "sim/simulation.py"; +const MODULE_NAME: &str = "simulation"; + +pub struct Model { + pub py_src: String, + pub amp_factor: u64, + pub balances: Vec, + pub n_coins: u8, + pub target_prices: Vec, + pub pool_tokens: u128, +} + +impl Model { + /// Constructs a new [`Model`]. + pub fn new(amp_factor: u64, balances: Vec, n_coins: u8) -> Model { + let src_file = File::open(FILE_PATH); + let mut src_file = match src_file { + Ok(file) => file, + Err(error) => { + panic!("{error:?}\n Please run `curl -L + https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`") + } + }; + let mut src_content = String::new(); + let _ = src_file.read_to_string(&mut src_content); + + assert_eq!(balances.len(), n_coins.into()); + + Self { + py_src: src_content, + amp_factor, + balances, + n_coins, + target_prices: vec![DEFAULT_TARGET_PRICE; n_coins.into()], + pool_tokens: DEFAULT_POOL_TOKENS, + } + } + + pub fn new_with_pool_tokens( + amp_factor: u64, + balances: Vec, + n_coins: u8, + pool_token_amount: u128, + ) -> Model { + let src_file = File::open(FILE_PATH); + let mut src_file = match src_file { + Ok(file) => file, + Err(error) => { + panic!("{error:?}\n Please run `curl -L + https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`") + } + }; + let mut src_content = String::new(); + let _ = src_file.read_to_string(&mut src_content); + + Self { + py_src: src_content, + amp_factor, + balances, + n_coins, + target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE], + pool_tokens: pool_token_amount, + } + } + + pub fn sim_d(&self) -> u128 { + let gil = Python::acquire_gil(); + return self + .call0(gil.python(), "D") + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "dy", (i, j, dx)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_exchange(&self, i: u128, j: u128, dx: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "exchange", (i, j, dx)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_xp(&self) -> Vec { + let gil = Python::acquire_gil(); + return self + .call0(gil.python(), "xp") + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_y(&self, i: u128, j: u128, x: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "y", (i, j, x)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_y_d(&self, i: u128, d: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "y_D", (i, d)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_remove_liquidity_imbalance(&self, amounts: Vec) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1( + gil.python(), + "remove_liquidity_imbalance", + PyTuple::new(gil.python(), amounts.to_vec()), + ) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_calc_withdraw_one_coin(&self, token_amount: u128, i: u128) -> (u128, u128) { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "calc_withdraw_one_coin", (token_amount, i)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + fn call0(&self, py: Python, method_name: &str) -> Result { + let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + let model = sim + .getattr("Curve")? + .call1(( + self.amp_factor, + self.balances.to_vec(), + self.n_coins, + self.target_prices.to_vec(), + self.pool_tokens, + )) + .unwrap() + .to_object(py); + let py_ret = model.as_ref(py).call_method0(method_name); + self.extract_py_ret(py, py_ret) + } + + fn call1( + &self, + py: Python, + method_name: &str, + args: impl IntoPy>, + ) -> Result { + let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + let model = sim + .getattr("Curve")? + .call1(( + self.amp_factor, + self.balances.to_vec(), + self.n_coins, + self.target_prices.to_vec(), + self.pool_tokens, + )) + .unwrap() + .to_object(py); + let py_ret = model.as_ref(py).call_method1(method_name, args); + self.extract_py_ret(py, py_ret) + } + + fn extract_py_ret(&self, py: Python, ret: PyResult<&PyAny>) -> Result { + match ret { + Ok(v) => v.extract(), + Err(e) => { + e.print_and_set_sys_last_vars(py); + panic!("Python exeuction failed.") + } + } + } + + pub fn print_src(&self) { + println!("{}", self.py_src); + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index c3135baf..8dee1fb5 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -2,8 +2,8 @@ use std::ops::Mul; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, StdError, StdResult, Storage, - Uint128, Uint256, + coin, ensure, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, StdError, StdResult, + Storage, Uint128, Uint256, }; use white_whale_std::fee::PoolFee; @@ -16,17 +16,25 @@ use crate::math::Decimal256Helper; /// The amount of iterations to perform when calculating the Newton-Raphson approximation. const NEWTON_ITERATIONS: u64 = 32; -// todo isn't this for the 3pool? shouldn't it be 3 -// the number of assets in the pool -pub const N_COINS: u8 = 3; +/// Encodes all results of swapping from a source token to a destination token. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SwapResult { + /// New amount of source token + pub new_source_amount: Uint128, + /// New amount of destination token + pub new_destination_amount: Uint128, + /// Amount of destination token swapped + pub amount_swapped: Uint128, +} fn calculate_stableswap_d( + n_coins: Uint256, offer_pool: Decimal256, ask_pool: Decimal256, amp: &u64, precision: u8, ) -> Result { - let n_coins = Decimal256::from_ratio(Uint256::from(N_COINS), Uint256::from_u128(1)); + let n_coins_decimal = Decimal256::from_ratio(n_coins, Uint256::one()); let sum_pools = offer_pool.checked_add(ask_pool)?; if sum_pools.is_zero() { @@ -35,10 +43,7 @@ fn calculate_stableswap_d( } // ann = amp * n_coins - let ann = Decimal256::from_ratio( - Uint256::from_u128((*amp).into()).checked_mul(Uint256::from(N_COINS))?, - 1u8, - ); + let ann = Decimal256::from_ratio(Uint256::from_u128((*amp).into()).checked_mul(n_coins)?, 1u8); // perform Newton-Raphson method let mut current_d = sum_pools; @@ -48,7 +53,7 @@ fn calculate_stableswap_d( let new_d = [offer_pool, ask_pool] .into_iter() .try_fold::<_, _, Result<_, ContractError>>(current_d, |acc, pool| { - let mul_pools = pool.checked_mul(n_coins)?; + let mul_pools = pool.checked_mul(n_coins_decimal)?; acc.checked_multiply_ratio(current_d, mul_pools) })?; @@ -56,12 +61,16 @@ fn calculate_stableswap_d( // current_d = ((ann * sum_pools + new_d * n_coins) * current_d) / ((ann - 1) * current_d + (n_coins + 1) * new_d) current_d = (ann .checked_mul(sum_pools)? - .checked_add(new_d.checked_mul(n_coins)?)? + .checked_add(new_d.checked_mul(n_coins_decimal)?)? .checked_mul(current_d)?) .checked_div( (ann.checked_sub(Decimal256::one())? .checked_mul(current_d)? - .checked_add(n_coins.checked_add(Decimal256::one())?.checked_mul(new_d)?))?, + .checked_add( + n_coins_decimal + .checked_add(Decimal256::one())? + .checked_mul(new_d)?, + ))?, )?; if current_d >= old_d { @@ -95,6 +104,7 @@ pub enum StableSwapDirection { /// Calculates the new pool amount given the current pools and swap size. pub fn calculate_stableswap_y( + n_coins: Uint256, offer_pool: Decimal256, ask_pool: Decimal256, offer_amount: Decimal256, @@ -102,9 +112,9 @@ pub fn calculate_stableswap_y( ask_precision: u8, direction: StableSwapDirection, ) -> Result { - let ann = Uint256::from_u128((*amp).into()).checked_mul(Uint256::from(N_COINS))?; + let ann = Uint256::from_u128((*amp).into()).checked_mul(n_coins)?; - let d = calculate_stableswap_d(offer_pool, ask_pool, amp, ask_precision)? + let d = calculate_stableswap_d(n_coins, offer_pool, ask_pool, amp, ask_precision)? .to_uint256_with_precision(u32::from(ask_precision))?; let pool_sum = match direction { @@ -114,8 +124,8 @@ pub fn calculate_stableswap_y( .to_uint256_with_precision(u32::from(ask_precision))?; let c = d - .checked_multiply_ratio(d, pool_sum.checked_mul(Uint256::from(N_COINS))?)? - .checked_multiply_ratio(d, ann.checked_mul(Uint256::from(N_COINS))?)?; + .checked_multiply_ratio(d, pool_sum.checked_mul(n_coins)?)? + .checked_multiply_ratio(d, ann.checked_mul(n_coins)?)?; let b = pool_sum.checked_add(d.checked_div(ann)?)?; @@ -141,8 +151,11 @@ pub fn calculate_stableswap_y( Err(ContractError::ConvergeError) } +#[allow(clippy::too_many_arguments)] /// computes a swap +#[allow(clippy::too_many_arguments)] pub fn compute_swap( + n_coins: Uint256, offer_pool: Uint128, ask_pool: Uint128, offer_amount: Uint128, @@ -180,6 +193,7 @@ pub fn compute_swap( let offer_amount = Decimal256::decimal_with_precision(offer_amount, offer_precision)?; let new_pool = calculate_stableswap_y( + n_coins, offer_pool, ask_pool, offer_amount, @@ -688,26 +702,27 @@ pub fn compute_d(amp_factor: &u64, deposits: &Vec) -> Option { // Newton's method to approximate D let mut d_prev: Uint256; let mut d: Uint256 = sum_x.into(); - for amount in amount_times_coins.into_iter() { - for _ in 0..256 { - let mut d_prod = d; + for _ in 0..256 { + let mut d_prod = d; + for amount in amount_times_coins.clone().into_iter() { d_prod = d_prod .checked_mul(d) .unwrap() .checked_div(amount.into()) .unwrap(); - d_prev = d; - d = compute_next_d(amp_factor, d, d_prod, sum_x, n_coins).unwrap(); - // Equality with the precision of 1 - if d > d_prev { - if d.checked_sub(d_prev).unwrap() <= Uint256::one() { - break; - } - } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { + } + d_prev = d; + d = compute_next_d(amp_factor, d, d_prod, sum_x, n_coins).unwrap(); + // Equality with the precision of 1 + if d > d_prev { + if d.checked_sub(d_prev).unwrap() <= Uint256::one() { break; } + } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { + break; } } + Some(d) } } @@ -782,3 +797,511 @@ pub fn compute_mint_amount_for_deposit( Some(Uint128::try_from(amount).unwrap()) } } + +/* +############ trying to fix tests +*/ + +/// Number of coins in a swap. Hardcoded to 3 to reuse previous tests +pub const N_COINS: u8 = 3; + +/// Compute the swap amount `y` in proportion to `x`. +/// +/// Solve for `y`: +/// +/// ```text +/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) +/// y**2 + b*y = c +/// ``` +#[allow(clippy::many_single_char_names, clippy::unwrap_used)] +pub fn compute_y_raw( + amp_factor: &u64, + swap_in: Uint128, + //swap_out: Uint128, + no_swap: Uint128, + d: Uint256, +) -> Option { + let ann = amp_factor.checked_mul(N_COINS.into())?; // A * n ** n + + // sum' = prod' = x + // c = D ** (n + 1) / (n ** (2 * n) * prod' * A) + let mut c = d; + + c = c + .checked_mul(d) + .unwrap() + .checked_div(swap_in.checked_mul(N_COINS.into()).unwrap().into()) + .unwrap(); + + c = c + .checked_mul(d) + .unwrap() + .checked_div(no_swap.checked_mul(N_COINS.into()).unwrap().into()) + .unwrap(); + c = c + .checked_mul(d) + .unwrap() + .checked_div(ann.checked_mul(N_COINS.into()).unwrap().into()) + .unwrap(); + // b = sum(swap_in, no_swap) + D // Ann - D + // not subtracting D here because that could result in a negative. + let b = d + .checked_div(ann.into()) + .unwrap() + .checked_add(swap_in.into()) + .unwrap() + .checked_add(no_swap.into()) + .unwrap(); + + // Solve for y by approximating: y**2 + b*y = c + let mut y_prev: Uint256; + let mut y = d; + for _ in 0..1000 { + y_prev = y; + // y = (y * y + c) / (2 * y + b - d); + let y_numerator = y.checked_mul(y).unwrap().checked_add(c).unwrap(); + let y_denominator = y + .checked_mul(Uint256::from(2u8)) + .unwrap() + .checked_add(b) + .unwrap() + .checked_sub(d) + .unwrap(); + y = y_numerator.checked_div(y_denominator).unwrap(); + if y > y_prev { + if y.checked_sub(y_prev).unwrap() <= Uint256::one() { + break; + } + } else if y_prev.checked_sub(y).unwrap() <= Uint256::one() { + break; + } + } + Some(y) +} + +/// Computes the swap amount `y` in proportion to `x`. +#[allow(clippy::unwrap_used)] +pub fn compute_y(amp_factor: &u64, x: Uint128, no_swap: Uint128, d: Uint256) -> Option { + let amount = compute_y_raw(&_factor, x, no_swap, d)?; + Some(Uint128::try_from(amount).unwrap()) +} + +/// Compute SwapResult after an exchange +#[allow(clippy::unwrap_used)] +pub fn swap_to( + amp_factor: &u64, + source_amount: Uint128, + swap_source_amount: Uint128, + swap_destination_amount: Uint128, + unswaped_amount: Uint128, +) -> Option { + let deposits = vec![ + coin(swap_source_amount.u128(), "denom1"), + coin(swap_destination_amount.u128(), "denom2"), + coin(unswaped_amount.u128(), "denom3"), + ]; + let y = compute_y( + amp_factor, + swap_source_amount.checked_add(source_amount).unwrap(), + unswaped_amount, + compute_d(amp_factor, &deposits).unwrap(), + )?; + // https://github.com/curvefi/curve-contract/blob/b0bbf77f8f93c9c5f4e415bce9cd71f0cdee960e/contracts/pool-templates/base/SwapTemplateBase.vy#L466 + let dy = swap_destination_amount + .checked_sub(y) + .unwrap() + .checked_sub(Uint128::one()) + .unwrap(); + + let amount_swapped = dy; + let new_destination_amount = swap_destination_amount.checked_sub(amount_swapped).unwrap(); + let new_source_amount = swap_source_amount.checked_add(source_amount).unwrap(); + + Some(SwapResult { + new_source_amount, + new_destination_amount, + amount_swapped, + }) +} + +#[cfg(test)] +#[allow( + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::too_many_arguments +)] +mod tests { + use cosmwasm_std::coin; + use proptest::prelude::*; + use rand::Rng; + use sim::Model; + + use super::*; + + /// Minimum amplification coefficient. + pub const MIN_AMP: u64 = 1; + + /// Maximum amplification coefficient. + pub const MAX_AMP: u64 = 1_000_000; + + /// Maximum number of tokens to swap at once. + pub const MAX_TOKENS_IN: Uint128 = Uint128::new(2u128 << 110); + + fn check_d(model: &Model, amount_a: u128, amount_b: u128, amount_c: u128) -> Uint256 { + let deposits = vec![ + coin(amount_a, "denom1"), + coin(amount_b, "denom2"), + coin(amount_c, "denom4"), + ]; + + let d = compute_d(&model.amp_factor, &deposits).unwrap(); + d + } + + fn check_y(model: &Model, swap_in: u128, no_swap: u128, d: Uint256) { + let y = compute_y_raw( + &model.amp_factor, + Uint128::new(swap_in), + Uint128::new(no_swap), + d, + ) + .unwrap(); + assert_eq!( + Uint128::try_from(y).unwrap().u128(), + model.sim_y(0, 1, swap_in) + ) + } + + #[test] + fn test_curve_math_specific() { + // Specific cases + let model_no_balance = Model::new(1, vec![0, 0, 0], N_COINS); + check_d(&model_no_balance, 0, 0, 0); + + let amount_a = 1046129065254161082u128; + let amount_b = 1250710035549196829u128; + let amount_c = 1111111111111111111u128; + let model = Model::new(1188, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = 2045250484898639148u128; + check_y(&model, amount_x, amount_c, d); + + let amount_a = 862538457714585493u128; + let amount_b = 492548187909826733u128; + let amount_c = 777777777777777777u128; + let model = Model::new(9, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = 815577754938955939u128; + + check_y(&model, amount_x, amount_c, d); + } + + #[test] + fn test_compute_mint_amount_for_deposit() { + let deposits = vec![ + coin(MAX_TOKENS_IN.u128(), "denom1"), + coin(MAX_TOKENS_IN.u128(), "denom2"), + coin(MAX_TOKENS_IN.u128(), "denom4"), + ]; + + let pool_assets = vec![ + coin(MAX_TOKENS_IN.u128(), "denom1"), + coin(MAX_TOKENS_IN.u128(), "denom2"), + coin(MAX_TOKENS_IN.u128(), "denom4"), + ]; + + let pool_token_supply = MAX_TOKENS_IN; + + let actual_mint_amount = + compute_mint_amount_for_deposit(&MIN_AMP, &deposits, &pool_assets, pool_token_supply) + .unwrap(); + let expected_mint_amount = MAX_TOKENS_IN; + + assert_eq!(actual_mint_amount, expected_mint_amount); + } + + #[ignore] + #[test] + fn test_curve_math_with_random_inputs() { + for _ in 0..100 { + let mut rng = rand::thread_rng(); + + let amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP); + let amount_a = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + let amount_b = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + let amount_c = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + println!("testing curve_math_with_random_inputs:"); + println!( + "amp_factor: {}, amount_a: {}, amount_b: {}, amount_c: {}", + amp_factor, amount_a, amount_b, amount_c, + ); + + let model = Model::new(amp_factor, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = rng.gen_range(0..=amount_a); + + println!("amount_x: {}", amount_x); + check_y(&model, amount_x, amount_c, d); + } + } + + #[derive(Debug)] + struct SwapTest { + pub amp_factor: u64, + pub swap_reserve_balance_a: Uint128, + pub swap_reserve_balance_b: Uint128, + pub swap_reserve_balance_c: Uint128, + pub user_token_balance_a: Uint128, + pub user_token_balance_b: Uint128, + } + + impl SwapTest { + pub fn swap_a_to_b(&mut self, swap_amount: Uint128) { + self.do_swap(true, swap_amount) + } + + pub fn swap_b_to_a(&mut self, swap_amount: Uint128) { + self.do_swap(false, swap_amount) + } + + fn do_swap(&mut self, swap_a_to_b: bool, source_amount: Uint128) { + let (swap_source_amount, swap_dest_amount) = match swap_a_to_b { + true => (self.swap_reserve_balance_a, self.swap_reserve_balance_b), + false => (self.swap_reserve_balance_b, self.swap_reserve_balance_a), + }; + + let SwapResult { + new_source_amount, + new_destination_amount, + amount_swapped, + .. + } = swap_to( + &self.amp_factor, + source_amount, + swap_source_amount, + swap_dest_amount, + self.swap_reserve_balance_c, + ) + .unwrap(); + + match swap_a_to_b { + true => { + self.swap_reserve_balance_a = new_source_amount; + self.swap_reserve_balance_b = new_destination_amount; + self.user_token_balance_a -= source_amount; + self.user_token_balance_b += amount_swapped; + } + false => { + self.swap_reserve_balance_a = new_destination_amount; + self.swap_reserve_balance_b = new_source_amount; + self.user_token_balance_a += amount_swapped; + self.user_token_balance_b -= source_amount; + } + } + } + } + + proptest! { + #[test] + fn test_swaps_does_not_result_in_more_tokens( + amp_factor in MIN_AMP..=MAX_AMP, + initial_user_token_a_amount in 10_000_000..MAX_TOKENS_IN.u128() >> 16, + initial_user_token_b_amount in 10_000_000..MAX_TOKENS_IN.u128() >> 16, + ) { + + let mut t = SwapTest { amp_factor, swap_reserve_balance_a: MAX_TOKENS_IN, swap_reserve_balance_b: MAX_TOKENS_IN, + swap_reserve_balance_c: MAX_TOKENS_IN, + user_token_balance_a: Uint128::new(initial_user_token_a_amount), + user_token_balance_b:Uint128::new(initial_user_token_b_amount), + }; + + const ITERATIONS: u64 = 100; + const SHRINK_MULTIPLIER: u64= 10; + + for i in 0..ITERATIONS { + let before_balance_a = t.user_token_balance_a; + let before_balance_b = t.user_token_balance_b; + let swap_amount = before_balance_a / Uint128::from((i + 1) * SHRINK_MULTIPLIER); + t.swap_a_to_b(swap_amount); + let after_balance = t.user_token_balance_a + t.user_token_balance_b; + + assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, amp_factor: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, amp_factor); + } + + for i in 0..ITERATIONS { + let before_balance_a = t.user_token_balance_a; + let before_balance_b = t.user_token_balance_b; + let swap_amount = before_balance_a / Uint128::from((i + 1) * SHRINK_MULTIPLIER); + t.swap_a_to_b(swap_amount); + let after_balance = t.user_token_balance_a + t.user_token_balance_b; + + assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, amp_factor: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, amp_factor); + } + } + } + + // #[test] + // fn test_swaps_does_not_result_in_more_tokens_specific_one() { + // const AMP_FACTOR: u64 = 324449; + // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(10_000_000_000u128); + + // let stable_swap = StableSwap { + // initial_amp_factor: AMP_FACTOR, + // target_amp_factor: AMP_FACTOR, + // current_ts: ZERO_TS, + // start_ramp_ts: ZERO_TS, + // stop_ramp_ts: ZERO_TS, + // }; + + // let mut t = SwapTest { + // stable_swap, + // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + // }; + + // t.swap_a_to_b(Uint128::new(2097152u128)); + // t.swap_a_to_b(Uint128::new(8053063680u128)); + // t.swap_a_to_b(Uint128::new(48u128)); + // assert!( + // t.user_token_balance_a + t.user_token_balance_b + // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + // ); + // } + + // #[test] + // fn test_swaps_does_not_result_in_more_tokens_specific_two() { + // const AMP_FACTOR: u64 = 186512; + // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + // let stable_swap = StableSwap { + // initial_amp_factor: AMP_FACTOR, + // target_amp_factor: AMP_FACTOR, + // current_ts: ZERO_TS, + // start_ramp_ts: ZERO_TS, + // stop_ramp_ts: ZERO_TS, + // }; + + // let mut t = SwapTest { + // stable_swap, + // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + // }; + + // t.swap_b_to_a(Uint128::new(33579101u128)); + // t.swap_a_to_b(Uint128::new(2097152u128)); + // assert!( + // t.user_token_balance_a + t.user_token_balance_b + // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + // ); + // } + + // #[test] + // fn test_swaps_does_not_result_in_more_tokens_specific_three() { + // const AMP_FACTOR: u64 = 1220; + // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + // let stable_swap = StableSwap { + // initial_amp_factor: AMP_FACTOR, + // target_amp_factor: AMP_FACTOR, + // current_ts: ZERO_TS, + // start_ramp_ts: ZERO_TS, + // stop_ramp_ts: ZERO_TS, + // }; + + // let mut t = SwapTest { + // stable_swap, + // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + // }; + + // t.swap_b_to_a(Uint128::from(65535u128)); + // t.swap_b_to_a(Uint128::from(6133503u128)); + // t.swap_a_to_b(Uint128::from(65535u128)); + // assert!( + // t.user_token_balance_a + t.user_token_balance_b + // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + // ); + // } + + // proptest! { + // #[test] + // fn test_virtual_price_does_not_decrease_from_deposit( + // current_ts in ZERO_TS..u64::MAX, + // amp_factor in MIN_AMP..=MAX_AMP, + // deposit_amount_a in 0..MAX_TOKENS_IN.u128() >> 2, + // deposit_amount_b in 0..MAX_TOKENS_IN.u128() >> 2, + // deposit_amount_c in 0..MAX_TOKENS_IN.u128() >> 2, + // swap_token_a_amount in 0..MAX_TOKENS_IN.u128(), + // swap_token_b_amount in 0..MAX_TOKENS_IN.u128(), + // swap_token_c_amount in 0..MAX_TOKENS_IN.u128(), + // pool_token_supply in 0..MAX_TOKENS_IN.u128(), + // ) { + // let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); + // let stop_ramp_ts = cmp::min(u64::MAX, current_ts + MIN_RAMP_DURATION); + // let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); + // let d0 = invariant.compute_d(Uint128::new(swap_token_a_amount), Uint128::new(swap_token_b_amount), Uint128::new(swap_token_c_amount)).unwrap(); + + // let mint_amount = invariant.compute_mint_amount_for_deposit( + // Uint128::new(deposit_amount_a), + // Uint128::new(deposit_amount_b), + // Uint128::new(deposit_amount_c), + // Uint128::new(swap_token_a_amount), + // Uint128::new(swap_token_b_amount), + // Uint128::new(swap_token_c_amount), + // Uint128::new(pool_token_supply), + // ); + // prop_assume!(mint_amount.is_some()); + + // let new_swap_token_a_amount = swap_token_a_amount + deposit_amount_a; + // let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; + // let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; + // let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); + // let d1 = invariant.compute_d(Uint128::new(new_swap_token_a_amount), Uint128::new(new_swap_token_b_amount), Uint128::new(new_swap_token_c_amount)).unwrap(); + + // assert!(d0 < d1); + // assert!(d0 / Uint256::from( pool_token_supply) <= d1 / Uint256::from( new_pool_token_supply)); + // } + // } + /* + proptest! { + #[test] + fn test_virtual_price_does_not_decrease_from_swap( + current_ts in ZERO_TS..i64::MAX, + amp_factor in MIN_AMP..=MAX_AMP, + source_token_amount in 0..MAX_TOKENS_IN, + swap_source_amount in 0..MAX_TOKENS_IN, + swap_destination_amount in 0..MAX_TOKENS_IN, + unswapped_amount in 0..MAX_TOKENS_IN, + ) { + let source_token_amount = source_token_amount; + let swap_source_amount = swap_source_amount; + let swap_destination_amount = swap_destination_amount; + let unswapped_amount = unswapped_amount; + + let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); + let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION); + let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); + let d0 = invariant.compute_d(swap_source_amount, swap_destination_amount, unswapped_amount).unwrap(); + + let swap_result = invariant.swap_to(source_token_amount, swap_source_amount, swap_destination_amount, unswapped_amount); + prop_assume!(swap_result.is_some()); + + let swap_result = swap_result.unwrap(); + let d1 = invariant.compute_d(swap_result.new_source_amount, swap_result.new_destination_amount, unswapped_amount).unwrap(); + + assert!(d0 <= d1); // Pool token supply not changed on swaps + } + }*/ +} diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 4ac77377..f4751a61 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -268,7 +268,8 @@ pub fn provide_liquidity( if total_share == Uint128::zero() { // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool - let min_lp_token_amount = MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u8); + let min_lp_token_amount = + MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(pool_assets.len() as u128); let share = Uint128::try_from(compute_d(amp_factor, &deposits).unwrap())? .checked_sub(min_lp_token_amount) .map_err(|_| { diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index 5a557d09..58948079 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use cosmwasm_std::{coin, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128}; +use cosmwasm_std::{coin, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256}; use white_whale_std::pool_manager::{ AssetDecimalsResponse, Config, PoolInfoResponse, PoolType, ReverseSimulationResponse, @@ -55,6 +55,7 @@ pub fn query_simulation( get_asset_indexes_in_pool(&pool_info, offer_asset.denom, ask_asset_denom)?; let swap_computation = helpers::compute_swap( + Uint256::from(pool_info.assets.len() as u128), offer_asset_in_pool.amount, ask_asset_in_pool.amount, offer_asset.amount, @@ -161,6 +162,7 @@ pub fn query_reverse_simulation( let max_precision = offer_decimal.max(ask_decimal); let new_offer_pool_amount = calculate_stableswap_y( + Uint256::from(pool_info.assets.len() as u128), offer_pool, ask_pool, before_fees, diff --git a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs index 01ba3019..21153e32 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Decimal, DepsMut, Uint128}; +use cosmwasm_std::{Coin, Decimal, DepsMut, Uint128, Uint256}; use white_whale_std::pool_manager::PoolInfo; use white_whale_std::pool_network::swap::assert_max_spread; @@ -58,6 +58,7 @@ pub fn perform_swap( // compute the swap let swap_computation = helpers::compute_swap( + Uint256::from(pool_info.assets.len() as u128), offer_asset_in_pool.amount, ask_asset_in_pool.amount, offer_asset.amount, diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml b/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml index ff1b9f75..261cde33 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml @@ -47,4 +47,4 @@ cosmwasm-schema.workspace = true [dev-dependencies] proptest = "1.0.0" rand = "0.8.4" -stable-swap-sim = { path = "./sim", version = "^0.1" } +stable-swap-sim1 = { path = "./sim", version = "^0.1" } diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml index e01fbc98..eb2b6968 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "stable-swap-sim" +name = "stable-swap-sim1" version = "0.1.0" description = "Simulations of the StableSwap invariant compared to Curve's reference implementation." authors = ["Paul Stelzig paul@irulast.com>"] From 249d0bc417c60f82b235c89b8c8573bddbe69457 Mon Sep 17 00:00:00 2001 From: nahem Date: Thu, 23 May 2024 19:16:20 +0200 Subject: [PATCH 08/51] feat(smart-contracts): test math for pool manager --- .../proptest-regressions/helpers.txt | 2 + .../liquidity_hub/pool-manager/src/helpers.rs | 295 +++++++++--------- .../src/stableswap_math/curve.rs | 13 + 3 files changed, 161 insertions(+), 149 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt index f09de62a..5ef4eb07 100644 --- a/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt +++ b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt @@ -5,3 +5,5 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 55857276de2241e3d09d36aba47854e0017db66f6c5a61e306b38ad0d3b8aeeb # shrinks to amp_factor = 1, initial_user_token_a_amount = 10000000, initial_user_token_b_amount = 10000000 +cc 33456e9a9f11bed69ac5171155ce7a64f73f912fcbfede19046989302d1b2da9 # shrinks to amp_factor = 10, deposit_amount_a = 0, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 0, swap_token_b_amount = 0, swap_token_c_amount = 1, pool_token_supply = 0 +cc 75c3b0922c450b034b92dc8c2ea87edff47c90bbede702d84c9fd9c672e2f31f # shrinks to amp_factor = 141, deposit_amount_a = 308442737939502983046195411808336, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 870112623450347049437652954298478, swap_token_b_amount = 501497230776538877048085549853566, swap_token_c_amount = 24063806364666791266594852039507, pool_token_supply = 2 diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 8dee1fb5..5db11ce5 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -765,13 +765,13 @@ fn compute_next_d( pub fn compute_mint_amount_for_deposit( amp_factor: &u64, deposits: &Vec, - pool_assets: &[Coin], + swaps: &Vec, pool_token_supply: Uint128, ) -> Option { // Initial invariant let d_0 = compute_d(amp_factor, deposits)?; - let new_balances: Vec = pool_assets + let new_balances: Vec = swaps .iter() .enumerate() .map(|(i, pool_asset)| { @@ -1140,168 +1140,165 @@ mod tests { } } - // #[test] - // fn test_swaps_does_not_result_in_more_tokens_specific_one() { - // const AMP_FACTOR: u64 = 324449; - // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); - // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(10_000_000_000u128); - - // let stable_swap = StableSwap { - // initial_amp_factor: AMP_FACTOR, - // target_amp_factor: AMP_FACTOR, - // current_ts: ZERO_TS, - // start_ramp_ts: ZERO_TS, - // stop_ramp_ts: ZERO_TS, - // }; - - // let mut t = SwapTest { - // stable_swap, - // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, - // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, - // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, - // }; - - // t.swap_a_to_b(Uint128::new(2097152u128)); - // t.swap_a_to_b(Uint128::new(8053063680u128)); - // t.swap_a_to_b(Uint128::new(48u128)); - // assert!( - // t.user_token_balance_a + t.user_token_balance_b - // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) - // ); - // } - - // #[test] - // fn test_swaps_does_not_result_in_more_tokens_specific_two() { - // const AMP_FACTOR: u64 = 186512; - // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); - // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); - - // let stable_swap = StableSwap { - // initial_amp_factor: AMP_FACTOR, - // target_amp_factor: AMP_FACTOR, - // current_ts: ZERO_TS, - // start_ramp_ts: ZERO_TS, - // stop_ramp_ts: ZERO_TS, - // }; - - // let mut t = SwapTest { - // stable_swap, - // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, - // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, - // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, - // }; - - // t.swap_b_to_a(Uint128::new(33579101u128)); - // t.swap_a_to_b(Uint128::new(2097152u128)); - // assert!( - // t.user_token_balance_a + t.user_token_balance_b - // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) - // ); - // } - - // #[test] - // fn test_swaps_does_not_result_in_more_tokens_specific_three() { - // const AMP_FACTOR: u64 = 1220; - // const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); - // const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); - - // let stable_swap = StableSwap { - // initial_amp_factor: AMP_FACTOR, - // target_amp_factor: AMP_FACTOR, - // current_ts: ZERO_TS, - // start_ramp_ts: ZERO_TS, - // stop_ramp_ts: ZERO_TS, - // }; - - // let mut t = SwapTest { - // stable_swap, - // swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, - // swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, - // user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, - // user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, - // }; - - // t.swap_b_to_a(Uint128::from(65535u128)); - // t.swap_b_to_a(Uint128::from(6133503u128)); - // t.swap_a_to_b(Uint128::from(65535u128)); - // assert!( - // t.user_token_balance_a + t.user_token_balance_b - // <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) - // ); - // } - - // proptest! { - // #[test] - // fn test_virtual_price_does_not_decrease_from_deposit( - // current_ts in ZERO_TS..u64::MAX, - // amp_factor in MIN_AMP..=MAX_AMP, - // deposit_amount_a in 0..MAX_TOKENS_IN.u128() >> 2, - // deposit_amount_b in 0..MAX_TOKENS_IN.u128() >> 2, - // deposit_amount_c in 0..MAX_TOKENS_IN.u128() >> 2, - // swap_token_a_amount in 0..MAX_TOKENS_IN.u128(), - // swap_token_b_amount in 0..MAX_TOKENS_IN.u128(), - // swap_token_c_amount in 0..MAX_TOKENS_IN.u128(), - // pool_token_supply in 0..MAX_TOKENS_IN.u128(), - // ) { - // let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); - // let stop_ramp_ts = cmp::min(u64::MAX, current_ts + MIN_RAMP_DURATION); - // let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); - // let d0 = invariant.compute_d(Uint128::new(swap_token_a_amount), Uint128::new(swap_token_b_amount), Uint128::new(swap_token_c_amount)).unwrap(); - - // let mint_amount = invariant.compute_mint_amount_for_deposit( - // Uint128::new(deposit_amount_a), - // Uint128::new(deposit_amount_b), - // Uint128::new(deposit_amount_c), - // Uint128::new(swap_token_a_amount), - // Uint128::new(swap_token_b_amount), - // Uint128::new(swap_token_c_amount), - // Uint128::new(pool_token_supply), - // ); - // prop_assume!(mint_amount.is_some()); - - // let new_swap_token_a_amount = swap_token_a_amount + deposit_amount_a; - // let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; - // let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; - // let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); - // let d1 = invariant.compute_d(Uint128::new(new_swap_token_a_amount), Uint128::new(new_swap_token_b_amount), Uint128::new(new_swap_token_c_amount)).unwrap(); - - // assert!(d0 < d1); - // assert!(d0 / Uint256::from( pool_token_supply) <= d1 / Uint256::from( new_pool_token_supply)); - // } - // } - /* + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_one() { + const AMP_FACTOR: u64 = 324449; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(10_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_a_to_b(Uint128::new(2097152u128)); + t.swap_a_to_b(Uint128::new(8053063680u128)); + t.swap_a_to_b(Uint128::new(48u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_two() { + const AMP_FACTOR: u64 = 186512; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_b_to_a(Uint128::new(33579101u128)); + t.swap_a_to_b(Uint128::new(2097152u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_three() { + const AMP_FACTOR: u64 = 1220; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_b_to_a(Uint128::from(65535u128)); + t.swap_b_to_a(Uint128::from(6133503u128)); + t.swap_a_to_b(Uint128::from(65535u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + proptest! { + #[test] + fn test_virtual_price_does_not_decrease_from_deposit( + amp_factor in MIN_AMP..=MAX_AMP, + deposit_amount_a in 0..MAX_TOKENS_IN.u128() >> 2, + deposit_amount_b in 0..MAX_TOKENS_IN.u128() >> 2, + deposit_amount_c in 0..MAX_TOKENS_IN.u128() >> 2, + swap_token_a_amount in 0..MAX_TOKENS_IN.u128(), + swap_token_b_amount in 0..MAX_TOKENS_IN.u128(), + swap_token_c_amount in 0..MAX_TOKENS_IN.u128(), + pool_token_supply in 0..MAX_TOKENS_IN.u128(), + ) { + let swaps = vec![ + coin(swap_token_a_amount, "denom1"), + coin(swap_token_b_amount, "denom2"), + coin(swap_token_c_amount, "denom3"), + ]; + + let d0 = compute_d(&_factor, &swaps).unwrap(); + + let deposits = vec![ + coin(deposit_amount_a, "denom1"), + coin(deposit_amount_b, "denom2"), + coin(deposit_amount_c, "denom3"), + ]; + + let mint_amount = compute_mint_amount_for_deposit( + &_factor, + &swaps, + &deposits, + Uint128::new(pool_token_supply), + ); + prop_assume!(mint_amount.is_some()); + + let new_swap_token_a_amount = swap_token_a_amount + deposit_amount_a; + let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; + let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; + let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); + + let new_swaps = vec![ + coin(new_swap_token_a_amount, "denom1"), + coin(new_swap_token_b_amount, "denom2"), + coin(new_swap_token_c_amount, "denom3"), + ]; + + let d1 = compute_d(&_factor, &new_swaps).unwrap(); + + assert!(d0 < d1); + assert!(d0 / Uint256::from( pool_token_supply) <= d1 / Uint256::from( new_pool_token_supply)); + } + } + proptest! { #[test] fn test_virtual_price_does_not_decrease_from_swap( - current_ts in ZERO_TS..i64::MAX, amp_factor in MIN_AMP..=MAX_AMP, - source_token_amount in 0..MAX_TOKENS_IN, - swap_source_amount in 0..MAX_TOKENS_IN, - swap_destination_amount in 0..MAX_TOKENS_IN, - unswapped_amount in 0..MAX_TOKENS_IN, + source_token_amount in 0..MAX_TOKENS_IN.u128(), + swap_source_amount in 0..MAX_TOKENS_IN.u128(), + swap_destination_amount in 0..MAX_TOKENS_IN.u128(), + unswapped_amount in 0..MAX_TOKENS_IN.u128(), ) { let source_token_amount = source_token_amount; let swap_source_amount = swap_source_amount; let swap_destination_amount = swap_destination_amount; let unswapped_amount = unswapped_amount; - let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); - let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION); - let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); - let d0 = invariant.compute_d(swap_source_amount, swap_destination_amount, unswapped_amount).unwrap(); + let deposits = vec![ + coin(swap_source_amount, "denom1"), + coin(swap_destination_amount, "denom2"), + coin(unswapped_amount, "denom3"), + ]; + + let d0 = compute_d(&_factor, &deposits).unwrap(); - let swap_result = invariant.swap_to(source_token_amount, swap_source_amount, swap_destination_amount, unswapped_amount); + let swap_result = swap_to(&_factor, source_token_amount.into(), swap_source_amount.into(), swap_destination_amount.into(), unswapped_amount.into()); prop_assume!(swap_result.is_some()); let swap_result = swap_result.unwrap(); - let d1 = invariant.compute_d(swap_result.new_source_amount, swap_result.new_destination_amount, unswapped_amount).unwrap(); + + let swaps = vec![ + coin(swap_result.new_source_amount.u128(), "denom1"), + coin(swap_result.new_destination_amount.u128(), "denom2"), + coin(unswapped_amount, "denom3"), + ]; + + let d1 = compute_d(&_factor, &swaps).unwrap(); assert!(d0 <= d1); // Pool token supply not changed on swaps } - }*/ + } } diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs index 3aec253e..9249a55c 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs @@ -167,6 +167,16 @@ impl StableSwap { let amount_b_times_coins = amount_b.checked_mul(N_COINS.into()).unwrap(); let amount_c_times_coins = amount_c.checked_mul(N_COINS.into()).unwrap(); + if amount_a_times_coins == Uint128::zero() + || amount_b_times_coins == Uint128::zero() + || amount_c_times_coins == Uint128::zero() + { + println!( + "amount_a_times_coins: {}, amount_b_times_coins: {}, amount_c_times_coins: {}", + amount_a_times_coins, amount_b_times_coins, amount_c_times_coins + ); + } + // Newton's method to approximate D let mut d_prev: Uint256; let mut d: Uint256 = sum_x.into(); @@ -217,6 +227,7 @@ impl StableSwap { ) -> Option { // Initial invariant let d_0 = self.compute_d(swap_amount_a, swap_amount_b, swap_amount_c)?; + let new_balances = [ swap_amount_a.checked_add(deposit_amount_a).unwrap(), swap_amount_b.checked_add(deposit_amount_b).unwrap(), @@ -776,6 +787,7 @@ mod tests { let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); let stop_ramp_ts = cmp::min(u64::MAX, current_ts + MIN_RAMP_DURATION); let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); + let d0 = invariant.compute_d(Uint128::new(swap_token_a_amount), Uint128::new(swap_token_b_amount), Uint128::new(swap_token_c_amount)).unwrap(); let mint_amount = invariant.compute_mint_amount_for_deposit( @@ -793,6 +805,7 @@ mod tests { let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); + let d1 = invariant.compute_d(Uint128::new(new_swap_token_a_amount), Uint128::new(new_swap_token_b_amount), Uint128::new(new_swap_token_c_amount)).unwrap(); assert!(d0 < d1); From 106de067b2d4bc21c6dbaac7ea698ee9ad9199a8 Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 27 May 2024 10:39:29 +0200 Subject: [PATCH 09/51] feat(smart-contracts): address PR comments --- contracts/liquidity_hub/pool-manager/src/helpers.rs | 1 + .../pool-manager/src/tests/integration_tests.rs | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 5db11ce5..ede5ae2f 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -489,6 +489,7 @@ pub struct OfferAmountComputation { pub osmosis_fee_amount: Uint128, } +// TODO: make this work with n_coins being dynamic pub fn assert_slippage_tolerance( slippage_tolerance: &Option, deposits: &[Coin], diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index f535ddcc..18a6c3af 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -2023,10 +2023,8 @@ mod swapping { ); } - // TODO: make sure stableswap test works when pool has ONLY 2 assets - #[test] - fn basic_swapping_test_stable_swap() { + fn basic_swapping_test_stable_swap_two_assets() { let mut suite = TestingSuite::default_with_balances(vec![ coin(1_000_000_001u128, "uwhale".to_string()), coin(1_000_000_000u128, "uluna".to_string()), @@ -2071,7 +2069,7 @@ mod swapping { extra_fees: vec![], }; - // Create a pool + // Create a stableswap pool with amp = 100 suite.instantiate_default().create_pool( creator.clone(), asset_infos, From 93bfa2df84e35cfc898ad28810e64e2c13c73a0f Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 27 May 2024 10:58:21 +0200 Subject: [PATCH 10/51] feat(smart-contracts): address PR comments --- .../pool-manager/src/liquidity/commands.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index f4751a61..1a2c9252 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -283,6 +283,16 @@ pub fn provide_liquidity( )); } + // mint the lp tokens to the contract + messages.push(white_whale_std::lp_common::mint_lp_token_msg( + liquidity_token.clone(), + &env.contract.address, + &env.contract.address, + // TODO: check if the minimum liquidity amount is correct. + // min_lp_token_amount VS MINIMUM_LIQUIDITY_AMOUNT + min_lp_token_amount, + )?); + share } else { let amount = compute_mint_amount_for_deposit( From ef3f1ca3717d0f05321e7b6c7035e11266942b61 Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 27 May 2024 11:07:54 +0200 Subject: [PATCH 11/51] feat(smart-contracts): address PR comments --- .../liquidity_hub/pool-manager/src/helpers.rs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index ede5ae2f..0e14b43e 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -799,13 +799,6 @@ pub fn compute_mint_amount_for_deposit( } } -/* -############ trying to fix tests -*/ - -/// Number of coins in a swap. Hardcoded to 3 to reuse previous tests -pub const N_COINS: u8 = 3; - /// Compute the swap amount `y` in proportion to `x`. /// /// Solve for `y`: @@ -816,13 +809,14 @@ pub const N_COINS: u8 = 3; /// ``` #[allow(clippy::many_single_char_names, clippy::unwrap_used)] pub fn compute_y_raw( + n_coins: u8, amp_factor: &u64, swap_in: Uint128, //swap_out: Uint128, no_swap: Uint128, d: Uint256, ) -> Option { - let ann = amp_factor.checked_mul(N_COINS.into())?; // A * n ** n + let ann = amp_factor.checked_mul(n_coins.into())?; // A * n ** n // sum' = prod' = x // c = D ** (n + 1) / (n ** (2 * n) * prod' * A) @@ -831,18 +825,18 @@ pub fn compute_y_raw( c = c .checked_mul(d) .unwrap() - .checked_div(swap_in.checked_mul(N_COINS.into()).unwrap().into()) + .checked_div(swap_in.checked_mul(n_coins.into()).unwrap().into()) .unwrap(); c = c .checked_mul(d) .unwrap() - .checked_div(no_swap.checked_mul(N_COINS.into()).unwrap().into()) + .checked_div(no_swap.checked_mul(n_coins.into()).unwrap().into()) .unwrap(); c = c .checked_mul(d) .unwrap() - .checked_div(ann.checked_mul(N_COINS.into()).unwrap().into()) + .checked_div(ann.checked_mul(n_coins.into()).unwrap().into()) .unwrap(); // b = sum(swap_in, no_swap) + D // Ann - D // not subtracting D here because that could result in a negative. @@ -882,14 +876,21 @@ pub fn compute_y_raw( /// Computes the swap amount `y` in proportion to `x`. #[allow(clippy::unwrap_used)] -pub fn compute_y(amp_factor: &u64, x: Uint128, no_swap: Uint128, d: Uint256) -> Option { - let amount = compute_y_raw(&_factor, x, no_swap, d)?; +pub fn compute_y( + n_coins: u8, + amp_factor: &u64, + x: Uint128, + no_swap: Uint128, + d: Uint256, +) -> Option { + let amount = compute_y_raw(n_coins, &_factor, x, no_swap, d)?; Some(Uint128::try_from(amount).unwrap()) } /// Compute SwapResult after an exchange #[allow(clippy::unwrap_used)] pub fn swap_to( + n_coins: u8, amp_factor: &u64, source_amount: Uint128, swap_source_amount: Uint128, @@ -902,6 +903,7 @@ pub fn swap_to( coin(unswaped_amount.u128(), "denom3"), ]; let y = compute_y( + n_coins, amp_factor, swap_source_amount.checked_add(source_amount).unwrap(), unswaped_amount, @@ -948,6 +950,9 @@ mod tests { /// Maximum number of tokens to swap at once. pub const MAX_TOKENS_IN: Uint128 = Uint128::new(2u128 << 110); + /// Number of coins in a swap. Hardcoded to 3 to reuse previous tests + pub const N_COINS: u8 = 3; + fn check_d(model: &Model, amount_a: u128, amount_b: u128, amount_c: u128) -> Uint256 { let deposits = vec![ coin(amount_a, "denom1"), @@ -961,6 +966,7 @@ mod tests { fn check_y(model: &Model, swap_in: u128, no_swap: u128, d: Uint256) { let y = compute_y_raw( + N_COINS, &model.amp_factor, Uint128::new(swap_in), Uint128::new(no_swap), @@ -1077,6 +1083,7 @@ mod tests { amount_swapped, .. } = swap_to( + N_COINS, &self.amp_factor, source_amount, swap_source_amount, @@ -1286,7 +1293,7 @@ mod tests { let d0 = compute_d(&_factor, &deposits).unwrap(); - let swap_result = swap_to(&_factor, source_token_amount.into(), swap_source_amount.into(), swap_destination_amount.into(), unswapped_amount.into()); + let swap_result = swap_to(N_COINS, &_factor, source_token_amount.into(), swap_source_amount.into(), swap_destination_amount.into(), unswapped_amount.into()); prop_assume!(swap_result.is_some()); let swap_result = swap_result.unwrap(); From 45c7cf64c2c4fadbab734600748a0ac2cb072ea5 Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 28 May 2024 15:05:32 +0200 Subject: [PATCH 12/51] fix(smart-contracts): update slippage tolerance to work with N coins -> WIP --- .../liquidity_hub/pool-manager/src/helpers.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 0e14b43e..e75e7595 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -490,10 +490,11 @@ pub struct OfferAmountComputation { } // TODO: make this work with n_coins being dynamic + pub fn assert_slippage_tolerance( slippage_tolerance: &Option, - deposits: &[Coin], - pools: &[Coin], + deposits: &Vec, + pools: &Vec, pool_type: PoolType, amount: Uint128, pool_token_supply: Uint128, @@ -505,14 +506,19 @@ pub fn assert_slippage_tolerance( } let one_minus_slippage_tolerance = Decimal256::one() - slippage_tolerance; - let deposits: [Uint256; 2] = [deposits[0].amount.into(), deposits[1].amount.into()]; - let pools: [Uint256; 2] = [pools[0].amount.into(), pools[1].amount.into()]; + let deposits: Vec = deposits.iter().map(|coin| coin.amount.into()).collect(); + let pools: Vec = pools.iter().map(|coin| coin.amount.into()).collect(); // Ensure each prices are not dropped as much as slippage tolerance rate match pool_type { PoolType::StableSwap { .. } => { - let pools_total = pools[0].checked_add(pools[1])?; - let deposits_total = deposits[0].checked_add(deposits[1])?; + // TODO: shouldn't be necessary to handle unwraps properly as they come from Uint128, but doublecheck! + let pools_total: Uint256 = pools + .into_iter() + .fold(Uint256::zero(), |acc, x| acc.checked_add(x).unwrap()); + let deposits_total: Uint256 = deposits + .into_iter() + .fold(Uint256::zero(), |acc, x| acc.checked_add(x).unwrap()); let pool_ratio = Decimal256::from_ratio(pools_total, pool_token_supply); let deposit_ratio = Decimal256::from_ratio(deposits_total, amount); From 0ea5afd271deac3d6dc9c39a45b345a013718125 Mon Sep 17 00:00:00 2001 From: nahem Date: Tue, 28 May 2024 15:08:25 +0200 Subject: [PATCH 13/51] fix(smart-contracts): update slippage tolerance to work with N coins --- contracts/liquidity_hub/pool-manager/src/error.rs | 3 +++ contracts/liquidity_hub/pool-manager/src/helpers.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index d8ba8036..656b6c84 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -162,6 +162,9 @@ pub enum ContractError { #[error("Invalid swap route: {0}")] InvalidSwapRoute(SwapRoute), + + #[error("Invalid pool assets length, expected {expected} got {actual}")] + InvalidPoolAssetsLength { expected: usize, actual: usize }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index e75e7595..e0b5902d 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -531,6 +531,12 @@ pub fn assert_slippage_tolerance( } } PoolType::ConstantProduct => { + if deposits.len() != 2 || pools.len() != 2 { + return Err(ContractError::InvalidPoolAssetsLength { + expected: 2, + actual: deposits.len(), + }); + } if Decimal256::from_ratio(deposits[0], deposits[1]) * one_minus_slippage_tolerance > Decimal256::from_ratio(pools[0], pools[1]) || Decimal256::from_ratio(deposits[1], deposits[0]) From 045a8f11c6d9443986d6035b73eb2366565bc3d6 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 6 May 2024 11:15:04 +0100 Subject: [PATCH 14/51] chore: bump cosmwasm-* versions --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc444d45..0eef6ed0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,9 +153,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmwasm-crypto" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9934c79e58d9676edfd592557dee765d2a6ef54c09d5aa2edb06156b00148966" +checksum = "e6b4c3f9c4616d6413d4b5fc4c270a4cc32a374b9be08671e80e1a019f805d8f" dependencies = [ "digest 0.10.7", "ecdsa", @@ -167,18 +167,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5e72e330bd3bdab11c52b5ecbdeb6a8697a004c57964caeb5d876f0b088b3c" +checksum = "c586ced10c3b00e809ee664a895025a024f60d65d34fe4c09daed4a4db68a3f3" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e3a2136e2a60e8b6582f5dffca5d1a683ed77bf38537d330bc1dfccd69010" +checksum = "8467874827d384c131955ff6f4d47d02e72a956a08eb3c0ff24f8c903a5517b4" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d803bea6bd9ed61bd1ee0b4a2eb09ee20dbb539cc6e0b8795614d20952ebb1" +checksum = "f6db85d98ac80922aef465e564d5b21fa9cfac5058cb62df7f116c3682337393" dependencies = [ "proc-macro2", "quote", @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8666e572a3a2519010dde88c04d16e9339ae751b56b2bb35081fe3f7d6be74" +checksum = "712fe58f39d55c812f7b2c84e097cdede3a39d520f89b6dc3153837e31741927" dependencies = [ "base64", "bech32", diff --git a/Cargo.toml b/Cargo.toml index c673c970..aec3958d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,8 @@ publish = false authors = ["White Whale Defi"] [workspace.dependencies] -cosmwasm-schema = { version = "1.5.3" } -cosmwasm-std = { version = "1.1.4", features = [ +cosmwasm-schema = { version = "1.5.4" } +cosmwasm-std = { version = "1.5.4", features = [ "iterator", "cosmwasm_1_2", "stargate", From da16403bd0b89a368f5dbce152cdc5b34245ba91 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 6 May 2024 12:34:49 +0100 Subject: [PATCH 15/51] refactor: remove the temporary FillRewardsCoin msg from the whalelair/bonding-manager --- .../bonding-manager/src/contract.rs | 3 +- .../bonding-manager/src/tests/robot.rs | 19 +++++------ .../epoch-manager/tests/epoch.rs | 1 - .../incentive-manager/src/helpers.rs | 2 +- .../src/position/commands.rs | 2 +- .../pool-manager/src/manager/commands.rs | 5 ++- .../pool-manager/src/router/commands.rs | 17 ++++------ .../pool-manager/src/swap/commands.rs | 17 +++------- .../vault-manager/src/manager/commands.rs | 3 +- .../vault-manager/src/router/commands.rs | 3 +- .../liquidity_hub/whale_lair/src/contract.rs | 5 +-- .../white-whale-std/src/bonding_manager.rs | 21 +++--------- packages/white-whale-std/src/whale_lair.rs | 32 +++---------------- 13 files changed, 38 insertions(+), 92 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 13ffc04b..b1d0eda9 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -104,8 +104,7 @@ pub fn execute( unbonding_period, growth_rate, ), - ExecuteMsg::FillRewards { .. } => commands::fill_rewards(deps, env, info), - ExecuteMsg::FillRewardsCoin => commands::fill_rewards(deps, env, info), + ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), ExecuteMsg::Claim { .. } => commands::claim(deps, env, info), ExecuteMsg::EpochChangedHook { current_epoch } => { // Epoch has been updated, update rewards bucket diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs index 3b79f5b6..0fdb5ce8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs @@ -21,7 +21,6 @@ use white_whale_std::bonding_manager::{ use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; use white_whale_std::pool_manager::PoolType; -use white_whale_std::pool_network::asset::PairType; pub fn bonding_manager_contract() -> Box> { let contract = ContractWrapper::new( @@ -36,11 +35,13 @@ pub fn bonding_manager_contract() -> Box> { } fn contract_pool_manager() -> Box> { - let contract = ContractWrapper::new_with_empty( + let contract = ContractWrapper::new( pool_manager::contract::execute, pool_manager::contract::instantiate, pool_manager::contract::query, - ); + ) + .with_migrate(pool_manager::contract::migrate) + .with_reply(pool_manager::contract::reply); Box::new(contract) } @@ -694,12 +695,12 @@ impl TestingRobot { funds: Vec, result: impl Fn(Result), ) -> &mut Self { - let msg = white_whale_std::bonding_manager::ExecuteMsg::FillRewardsCoin {}; - - result( - self.app - .execute_contract(sender, self.bonding_manager_addr.clone(), &msg, &funds), - ); + result(self.app.execute_contract( + sender, + self.bonding_manager_addr.clone(), + &ExecuteMsg::FillRewards, + &funds, + )); self } diff --git a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs index fde760aa..28b87905 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs @@ -2,7 +2,6 @@ use cosmwasm_std::from_json; use cosmwasm_std::testing::{mock_env, mock_info}; use epoch_manager::contract::{execute, query}; -use epoch_manager::ContractError; use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochResponse, ExecuteMsg, QueryMsg}; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::pool_network::mock_querier::mock_dependencies; diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 997bf7bc..1f6dabe4 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -71,7 +71,7 @@ pub(crate) fn process_incentive_creation_fee( } // send incentive creation fee to whale lair for distribution - messages.push(white_whale_std::whale_lair::fill_rewards_msg_coin( + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( config.whale_lair_addr.clone().into_string(), vec![incentive_creation_fee.to_owned()], )?); diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index 0b382d34..e668599b 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -245,7 +245,7 @@ pub(crate) fn withdraw_position( let whale_lair_addr = CONFIG.load(deps.storage)?.whale_lair_addr; // send penalty to whale lair for distribution - messages.push(white_whale_std::whale_lair::fill_rewards_msg_coin( + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( whale_lair_addr.into_string(), vec![penalty], )?); diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index b476a744..9c19b832 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -12,7 +12,6 @@ use crate::{ use white_whale_std::lp_common::LP_SYMBOL; use white_whale_std::pool_manager::{PoolInfo, PoolType}; -use white_whale_std::whale_lair::fill_rewards_msg_coin; pub const MAX_ASSETS_PER_POOL: usize = 4; @@ -108,8 +107,8 @@ pub fn create_pool( // send pool creation fee to whale lair let creation_fee = vec![config.pool_creation_fee]; - // send pool creation fee to whale lair i.e the new fee_collector - messages.push(fill_rewards_msg_coin( + // send pool creation fee to the bonding manager + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( config.bonding_manager_addr.into_string(), creation_fee, )?); diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index abf9b180..74c51371 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -1,10 +1,9 @@ use cosmwasm_std::{ - attr, coin, ensure, wasm_execute, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, - MessageInfo, Response, Uint128, + attr, coin, ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, MessageInfo, Response, + Uint128, }; use white_whale_std::common::validate_addr_or_default; use white_whale_std::pool_manager::{SwapOperation, SwapRoute}; -use white_whale_std::whale_lair; use crate::queries::simulate_swap_operations; use crate::{ @@ -123,14 +122,10 @@ pub fn execute_swap_operations( })); } if !swap_result.protocol_fee_asset.amount.is_zero() { - fee_messages.push( - wasm_execute( - config.bonding_manager_addr.to_string(), - &whale_lair::ExecuteMsg::FillRewardsCoin {}, - vec![swap_result.protocol_fee_asset.clone()], - )? - .into(), - ); + fee_messages.push(white_whale_std::bonding_manager::fill_rewards_msg( + config.bonding_manager_addr.to_string(), + vec![swap_result.protocol_fee_asset.clone()], + )?); } } } diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index cd101024..870a245d 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -1,14 +1,11 @@ use crate::{state::CONFIG, ContractError}; -use cosmwasm_std::{ - ensure, wasm_execute, Addr, BankMsg, CosmosMsg, DepsMut, MessageInfo, Response, -}; +use cosmwasm_std::{ensure, Addr, BankMsg, CosmosMsg, DepsMut, MessageInfo, Response}; pub const MAX_ASSETS_PER_POOL: usize = 4; use crate::state::get_pool_by_identifier; use cosmwasm_std::Decimal; use white_whale_std::common::validate_addr_or_default; -use white_whale_std::whale_lair; use super::perform_swap::perform_swap; @@ -73,14 +70,10 @@ pub fn swap( } if !swap_result.protocol_fee_asset.amount.is_zero() { - messages.push( - wasm_execute( - config.bonding_manager_addr.to_string(), - &whale_lair::ExecuteMsg::FillRewardsCoin {}, - vec![swap_result.protocol_fee_asset.clone()], - )? - .into(), - ); + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( + config.bonding_manager_addr.to_string(), + vec![swap_result.protocol_fee_asset.clone()], + )?); } println!("messages: {:?}", messages); diff --git a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs index e6108b94..fe9d1e1c 100644 --- a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs @@ -5,7 +5,6 @@ use cosmwasm_std::{ use white_whale_std::lp_common::LP_SYMBOL; use white_whale_std::tokenfactory; use white_whale_std::vault_manager::{Vault, VaultFee}; -use white_whale_std::whale_lair::fill_rewards_msg_coin; use crate::state::{get_vault_by_identifier, CONFIG, VAULTS, VAULT_COUNTER}; use crate::ContractError; @@ -40,7 +39,7 @@ pub fn create_vault( ); // send protocol fee to whale lair - messages.push(fill_rewards_msg_coin( + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( config.whale_lair_addr.into_string(), creation_fee, )?); diff --git a/contracts/liquidity_hub/vault-manager/src/router/commands.rs b/contracts/liquidity_hub/vault-manager/src/router/commands.rs index 8b3e43dd..448f1f14 100644 --- a/contracts/liquidity_hub/vault-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/vault-manager/src/router/commands.rs @@ -4,7 +4,6 @@ use cosmwasm_std::{ }; use white_whale_std::vault_manager::{CallbackMsg, ExecuteMsg}; -use white_whale_std::whale_lair::fill_rewards_msg_coin; use crate::helpers::query_balances; use crate::state::{get_vault_by_identifier, CONFIG, ONGOING_FLASHLOAN, TEMP_BALANCES, VAULTS}; @@ -214,7 +213,7 @@ pub fn after_flashloan( let config = CONFIG.load(deps.storage)?; // send protocol fee to whale lair - messages.push(fill_rewards_msg_coin( + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( config.whale_lair_addr.into_string(), coins(protocol_fee.u128(), loan_asset.denom), )?); diff --git a/contracts/liquidity_hub/whale_lair/src/contract.rs b/contracts/liquidity_hub/whale_lair/src/contract.rs index 97c63080..a659e0e0 100644 --- a/contracts/liquidity_hub/whale_lair/src/contract.rs +++ b/contracts/liquidity_hub/whale_lair/src/contract.rs @@ -93,14 +93,11 @@ pub fn execute( growth_rate, fee_distributor_addr, ), - ExecuteMsg::FillRewards { .. } => { + ExecuteMsg::FillRewards => { //unimplemented!(); //todo deposit in next epoch Ok(Response::default().add_attributes(vec![("action", "fill_rewards".to_string())])) } - ExecuteMsg::FillRewardsCoin => { - Ok(Response::default().add_attributes(vec![("action", "fill_rewards".to_string())])) - } } } diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 728d1f31..cfa5a9e7 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -1,7 +1,4 @@ -use crate::{ - epoch_manager::epoch_manager::Epoch as EpochV2, - pool_network::asset::{Asset, ToCoins}, -}; +use crate::epoch_manager::epoch_manager::Epoch as EpochV2; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ @@ -120,14 +117,8 @@ pub enum ExecuteMsg { }, Claim {}, - /// V2 MESSAGES - - /// Fills the whale lair with new rewards. - FillRewards { - assets: Vec, - }, /// Fills the whale lair with new rewards. - FillRewardsCoin, + FillRewards, /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) EpochChangedHook { @@ -221,13 +212,11 @@ pub struct BondingWeightResponse { } /// Creates a message to fill rewards on the whale lair contract. -pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult { +pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult { Ok(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr, - msg: to_json_binary(&ExecuteMsg::FillRewards { - assets: assets.to_coins()?, - })?, - funds: assets.to_coins()?, + msg: to_json_binary(&ExecuteMsg::FillRewards)?, + funds: assets, })) } diff --git a/packages/white-whale-std/src/whale_lair.rs b/packages/white-whale-std/src/whale_lair.rs index 9212e417..58b82932 100644 --- a/packages/white-whale-std/src/whale_lair.rs +++ b/packages/white-whale-std/src/whale_lair.rs @@ -1,8 +1,6 @@ -use crate::pool_network::asset::{Asset, AssetInfo, ToCoins}; +use crate::pool_network::asset::{Asset, AssetInfo}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ - to_json_binary, Addr, Coin, CosmosMsg, Decimal, StdResult, Timestamp, Uint128, Uint64, WasmMsg, -}; +use cosmwasm_std::{Addr, Decimal, Timestamp, Uint128, Uint64}; #[cw_serde] pub struct Config { @@ -83,12 +81,9 @@ pub enum ExecuteMsg { fee_distributor_addr: Option, }, - /// V2 MESSAGES - - /// Fills the whale lair with new rewards. - FillRewards { assets: Vec }, + //todo remove when the tests are using the bonding-manager instead of whale-lair /// Fills the whale lair with new rewards. - FillRewardsCoin, + FillRewards, } #[cw_serde] @@ -167,22 +162,3 @@ pub struct BondingWeightResponse { pub share: Decimal, pub timestamp: Timestamp, } - -/// 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 { - contract_addr, - msg: to_json_binary(&ExecuteMsg::FillRewards { - assets: assets.clone(), - })?, - funds: assets.to_coins()?, - })) -} - -pub fn fill_rewards_msg_coin(contract_addr: String, rewards: Vec) -> StdResult { - Ok(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg: to_json_binary(&ExecuteMsg::FillRewardsCoin)?, - funds: rewards, - })) -} From a4bbed0f24eb51bd61693e4e01979d8af900e713 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 6 May 2024 12:40:29 +0100 Subject: [PATCH 16/51] refactor: abstract bank burn msg --- .../liquidity_hub/pool-manager/src/router/commands.rs | 8 +++----- contracts/liquidity_hub/pool-manager/src/swap/commands.rs | 7 ++----- packages/white-whale-std/src/coin.rs | 7 ++++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index 74c51371..16cc0ee8 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ - attr, coin, ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, MessageInfo, Response, - Uint128, + attr, coin, ensure, Addr, BankMsg, Coin, Decimal, DepsMut, MessageInfo, Response, Uint128, }; +use white_whale_std::coin::burn_coin_msg; use white_whale_std::common::validate_addr_or_default; use white_whale_std::pool_manager::{SwapOperation, SwapRoute}; @@ -117,9 +117,7 @@ pub fn execute_swap_operations( // add the fee messages if !swap_result.burn_fee_asset.amount.is_zero() { - fee_messages.push(CosmosMsg::Bank(BankMsg::Burn { - amount: vec![swap_result.burn_fee_asset], - })); + fee_messages.push(burn_coin_msg(swap_result.burn_fee_asset)); } if !swap_result.protocol_fee_asset.amount.is_zero() { fee_messages.push(white_whale_std::bonding_manager::fill_rewards_msg( diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index 870a245d..a4bbeb53 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -5,6 +5,7 @@ pub const MAX_ASSETS_PER_POOL: usize = 4; use crate::state::get_pool_by_identifier; use cosmwasm_std::Decimal; +use white_whale_std::coin::burn_coin_msg; use white_whale_std::common::validate_addr_or_default; use super::perform_swap::perform_swap; @@ -64,9 +65,7 @@ pub fn swap( } if !swap_result.burn_fee_asset.amount.is_zero() { - messages.push(CosmosMsg::Bank(BankMsg::Burn { - amount: vec![swap_result.burn_fee_asset.clone()], - })); + messages.push(burn_coin_msg(swap_result.burn_fee_asset.clone())); } if !swap_result.protocol_fee_asset.amount.is_zero() { @@ -76,8 +75,6 @@ pub fn swap( )?); } - println!("messages: {:?}", messages); - Ok(Response::new().add_messages(messages).add_attributes(vec![ ("action", "swap".to_string()), ("sender", sender.into_string()), diff --git a/packages/white-whale-std/src/coin.rs b/packages/white-whale-std/src/coin.rs index 0b927b59..3224fea1 100644 --- a/packages/white-whale-std/src/coin.rs +++ b/packages/white-whale-std/src/coin.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use cosmwasm_std::{Coin, StdError, StdResult, Uint128}; +use cosmwasm_std::{BankMsg, Coin, CosmosMsg, StdError, StdResult, Uint128}; #[cfg(feature = "injective")] pub const PEGGY_PREFIX: &str = "peggy"; @@ -197,3 +197,8 @@ pub fn aggregate_coins(coins: Vec) -> StdResult> { Ok(aggregated_coins) } + +/// Creates a CosmosMsg::Bank::BankMsg::Burn message with the given coin. +pub fn burn_coin_msg(coin: Coin) -> CosmosMsg { + CosmosMsg::Bank(BankMsg::Burn { amount: vec![coin] }) +} From 6a5522e936748ff12f57d8a64a810a943639c946 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 6 May 2024 16:43:43 +0100 Subject: [PATCH 17/51] refactor: make epoch id u64 on epoch manager --- .../schema/bonding-manager.json | 27 +------------ .../bonding-manager/schema/raw/execute.json | 27 +------------ .../bonding-manager/src/tests/robot.rs | 6 +-- .../epoch-manager/src/commands.rs | 38 +++++++++---------- .../epoch-manager/src/contract.rs | 8 +--- .../epoch-manager/src/queries.rs | 2 +- .../liquidity_hub/epoch-manager/src/state.rs | 2 +- .../epoch-manager/tests/epoch.rs | 4 +- .../src/epoch_manager/epoch_manager.rs | 2 +- 9 files changed, 30 insertions(+), 86 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index cdfa3a35..63d83d38 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -185,36 +185,11 @@ }, "additionalProperties": false }, - { - "description": "V2 MESSAGES Fills the whale lair with new rewards.", - "type": "object", - "required": [ - "fill_rewards" - ], - "properties": { - "fill_rewards": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Fills the whale lair with new rewards.", "type": "string", "enum": [ - "fill_rewards_coin" + "fill_rewards" ] }, { diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index 77b99cb6..bbc26076 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -121,36 +121,11 @@ }, "additionalProperties": false }, - { - "description": "V2 MESSAGES Fills the whale lair with new rewards.", - "type": "object", - "required": [ - "fill_rewards" - ], - "properties": { - "fill_rewards": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Fills the whale lair with new rewards.", "type": "string", "enum": [ - "fill_rewards_coin" + "fill_rewards" ] }, { diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs index 0fdb5ce8..d2e422a8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs @@ -197,8 +197,7 @@ impl TestingRobot { println!("hook_registration_msg: {:?}", resp); // self.fast_forward(10); - let new_epoch_msg = - white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch {}; + let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; self.app .execute_contract( self.sender.clone(), @@ -328,8 +327,7 @@ impl TestingRobot { } pub(crate) fn create_new_epoch(&mut self) -> &mut Self { - let new_epoch_msg = - white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch {}; + let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; self.app .execute_contract( self.sender.clone(), diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index e5f9d779..67553aee 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -27,26 +27,30 @@ pub(crate) fn remove_hook( } /// Creates a new epoch. -pub fn create_epoch( - deps: DepsMut, - _env: Env, - info: MessageInfo, -) -> Result { +pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result { cw_utils::nonpayable(&info)?; let mut current_epoch = query_current_epoch(deps.as_ref())?.epoch; let config = CONFIG.load(deps.storage)?; println!("Creating new epoch"); - // if env - // .block - // .time - // .minus_nanos(current_epoch.start_time.nanos()) - // .nanos() - // < config.epoch_config.duration.u64() - // { - // return Err(ContractError::CurrentEpochNotExpired); - // } + println!("Current epoch: {:?}", current_epoch); + println!( + "env + .block + .time: {:?}", + env.block.time + ); + + if env + .block + .time + .minus_nanos(current_epoch.start_time.nanos()) + .nanos() + < config.epoch_config.duration.u64() + { + return Err(ContractError::CurrentEpochNotExpired); + } println!("Creating new epoch"); current_epoch.id = current_epoch .id @@ -56,11 +60,7 @@ pub fn create_epoch( .start_time .plus_nanos(config.epoch_config.duration.u64()); - EPOCHS.save( - deps.storage, - ¤t_epoch.id.to_be_bytes(), - ¤t_epoch, - )?; + EPOCHS.save(deps.storage, current_epoch.id, ¤t_epoch)?; let messages = HOOKS.prepare_hooks(deps.storage, |hook| { EpochChangedHookMsg { diff --git a/contracts/liquidity_hub/epoch-manager/src/contract.rs b/contracts/liquidity_hub/epoch-manager/src/contract.rs index 82075319..f56c7041 100644 --- a/contracts/liquidity_hub/epoch-manager/src/contract.rs +++ b/contracts/liquidity_hub/epoch-manager/src/contract.rs @@ -35,11 +35,7 @@ pub fn instantiate( } ADMIN.set(deps.branch(), Some(info.sender))?; - EPOCHS.save( - deps.storage, - &msg.start_epoch.id.to_be_bytes(), - &msg.start_epoch, - )?; + EPOCHS.save(deps.storage, msg.start_epoch.id, &msg.start_epoch)?; CONFIG.save( deps.storage, @@ -69,7 +65,7 @@ pub fn execute( ExecuteMsg::RemoveHook { contract_addr } => { commands::remove_hook(deps, info, api, &contract_addr) } - ExecuteMsg::CreateEpoch {} => commands::create_epoch(deps, env, info), + ExecuteMsg::CreateEpoch => commands::create_epoch(deps, env, info), ExecuteMsg::UpdateConfig { owner, epoch_config, diff --git a/contracts/liquidity_hub/epoch-manager/src/queries.rs b/contracts/liquidity_hub/epoch-manager/src/queries.rs index 560d1c0a..cca9a137 100644 --- a/contracts/liquidity_hub/epoch-manager/src/queries.rs +++ b/contracts/liquidity_hub/epoch-manager/src/queries.rs @@ -33,7 +33,7 @@ pub(crate) fn query_current_epoch(deps: Deps) -> StdResult { /// Queries the current epoch. Returns an [EpochResponse]. pub(crate) fn query_epoch(deps: Deps, id: u64) -> StdResult { let epoch = EPOCHS - .may_load(deps.storage, &id.to_be_bytes())? + .may_load(deps.storage, id)? .ok_or_else(|| StdError::generic_err(format!("No epoch found with id {}", id)))?; Ok(epoch.to_epoch_response()) } diff --git a/contracts/liquidity_hub/epoch-manager/src/state.rs b/contracts/liquidity_hub/epoch-manager/src/state.rs index ec9010c3..da4fa8db 100644 --- a/contracts/liquidity_hub/epoch-manager/src/state.rs +++ b/contracts/liquidity_hub/epoch-manager/src/state.rs @@ -5,4 +5,4 @@ use white_whale_std::epoch_manager::epoch_manager::{Config, Epoch}; pub const CONFIG: Item = Item::new("config"); pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("hooks"); -pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); +pub const EPOCHS: Map = Map::new("epochs"); diff --git a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs index 28b87905..03e6d5e2 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs @@ -22,7 +22,7 @@ fn create_new_epoch_successfully() { // move time ahead so we can create the epoch env.block.time = env.block.time.plus_nanos(86400); - let msg = ExecuteMsg::CreateEpoch {}; + let msg = ExecuteMsg::CreateEpoch; let res = execute(deps.as_mut(), env, info, msg).unwrap(); let query_res = query(deps.as_ref(), mock_env(), QueryMsg::CurrentEpoch {}).unwrap(); @@ -71,7 +71,7 @@ fn create_new_epoch_successfully() { // // move time ahead but not enough so the epoch creation fails // env.block.time = env.block.time.plus_nanos(86300); -// let msg = ExecuteMsg::CreateEpoch {}; +// let msg = ExecuteMsg::CreateEpoch; // let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); // match err { // ContractError::CurrentEpochNotExpired => {} diff --git a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs index 744d6100..13bc8885 100644 --- a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs +++ b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs @@ -14,7 +14,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { - CreateEpoch {}, + CreateEpoch, AddHook { contract_addr: String, }, From 6c8ea5bd53bf41fa6f327c9b838db2447c5d7541 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 6 May 2024 17:53:05 +0100 Subject: [PATCH 18/51] test: use the bonding-manager in pool-manager tests --- Cargo.lock | 1 + Cargo.toml | 1 + .../bonding-manager/src/commands.rs | 122 ++++--- .../bonding-manager/src/contract.rs | 14 +- .../bonding-manager/src/helpers.rs | 31 +- .../epoch-manager/src/commands.rs | 1 - .../liquidity_hub/pool-manager/Cargo.toml | 1 + .../liquidity_hub/pool-manager/src/queries.rs | 1 - .../src/tests/integration_tests.rs | 340 +++++++++++------- .../pool-manager/src/tests/suite.rs | 149 ++++++-- 10 files changed, 447 insertions(+), 214 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0eef6ed0..74c8a406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,6 +952,7 @@ name = "pool-manager" version = "0.1.0" dependencies = [ "anyhow", + "bonding-manager", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", diff --git a/Cargo.toml b/Cargo.toml index aec3958d..19559208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ incentive-factory = { path = "./contracts/liquidity_hub/pool-network/incentive_f terraswap-token = { path = "./contracts/liquidity_hub/pool-network/terraswap_token" } terraswap-pair = { path = "./contracts/liquidity_hub/pool-network/terraswap_pair" } incentive-manager = { path = "./contracts/liquidity_hub/incentive-manager" } +bonding-manager = { path = "./contracts/liquidity_hub/bonding-manager" } [workspace.metadata.dylint] libraries = [{ git = "https://github.com/0xFable/cw-lint" }] diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 01b0fa6e..25333f17 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -2,9 +2,9 @@ use cosmwasm_std::{ ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, SubMsg, Timestamp, Uint128, Uint64, }; -use white_whale_std::pool_network::asset; use white_whale_std::bonding_manager::Bond; +use white_whale_std::pool_network::asset; use crate::helpers::validate_growth_rate; use crate::queries::{get_current_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; @@ -325,63 +325,71 @@ pub fn claim(deps: DepsMut, _env: Env, info: MessageInfo) -> Result Result { + // Finding the most recent EpochID + let most_recent_epoch_id = match EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .next() { - // Finding the most recent EpochID - let most_recent_epoch_id = match EPOCHS - .keys(deps.storage, None, None, Order::Descending) - .next() - { - Some(epoch_id) => epoch_id?, - None => return Err(ContractError::Unauthorized {}), - }; - - let config = CONFIG.load(deps.storage)?; - let distribution_denom = config.distribution_denom.clone(); - - let mut messages: Vec = vec![]; - let mut submessages: Vec = vec![]; - // swap non-whale to whale - // Search info funds for LP tokens, LP tokens will contain LP_SYMBOL from lp_common and the string .pair. - let mut whale = info - .funds - .iter() - .find(|coin| coin.denom.eq(distribution_denom.as_str())) - .unwrap_or(&Coin { - denom: distribution_denom.clone(), - amount: Uint128::zero(), - }) - .to_owned(); - // Each of these helpers will add messages to the messages vector - // and may increment the whale Coin above with the result of the swaps - helpers::handle_lp_tokens(&info, &config, &mut submessages)?; - helpers::swap_coins_to_main_token( - info.funds.clone(), - &deps, - config, - &mut whale, - &distribution_denom, - &mut messages, - )?; - // Add the whale to the funds, the whale figure now should be the result - // of all the LP token withdrawals and swaps - // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned - // If this became an issue would could look at replys instead of the query - EPOCHS.update( - deps.storage, - &most_recent_epoch_id, - |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - }, - )?; - Ok(Response::default() - .add_messages(messages) - .add_submessages(submessages) - .add_attributes(vec![("action", "fill_rewards".to_string())])) - } + Some(epoch_id) => epoch_id?, + None => return Err(ContractError::Unauthorized {}), + }; + + let config = CONFIG.load(deps.storage)?; + let distribution_denom = config.distribution_denom.clone(); + + let mut messages: Vec = vec![]; + let mut submessages: Vec = vec![]; + // swap non-whale to whale + // Search info funds for LP tokens, LP tokens will contain LP_SYMBOL from lp_common and the string .pair. + let mut whale = info + .funds + .iter() + .find(|coin| coin.denom.eq(distribution_denom.as_str())) + .unwrap_or(&Coin { + denom: distribution_denom.clone(), + amount: Uint128::zero(), + }) + .to_owned(); + + // coins that are laying in the contract and have not been swapped before for lack of swap routes + let remanent_coins = deps + .querier + .query_all_balances(env.contract.address)? + .into_iter() + .filter(|coin| coin.denom.ne(distribution_denom.as_str())) + .collect::>(); + + println!("remanent_coins: {:?}", remanent_coins); + // Each of these helpers will add messages to the messages vector + // and may increment the whale Coin above with the result of the swaps + helpers::handle_lp_tokens(&info, &config, &mut submessages)?; + helpers::swap_coins_to_main_token( + remanent_coins, + &deps, + config, + &mut whale, + &distribution_denom, + &mut messages, + )?; + // Add the whale to the funds, the whale figure now should be the result + // of all the LP token withdrawals and swaps + // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned + // If this became an issue would could look at replys instead of the query + EPOCHS.update( + deps.storage, + &most_recent_epoch_id, + |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; + bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; + Ok(bucket) + }, + )?; + Ok(Response::default() + .add_messages(messages) + .add_submessages(submessages) + .add_attributes(vec![("action", "fill_rewards".to_string())])) } diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index b1d0eda9..bc30b780 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -107,6 +107,7 @@ pub fn execute( ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), ExecuteMsg::Claim { .. } => commands::claim(deps, env, info), ExecuteMsg::EpochChangedHook { current_epoch } => { + println!("EpochChangedHook: {:?}", current_epoch); // Epoch has been updated, update rewards bucket // and forward the expiring epoch // Store epoch manager and verify the sender is him @@ -149,6 +150,17 @@ pub fn execute( ..Epoch::default() }, )?; + + let all_epochs: Vec = EPOCHS + .range(deps.storage, None, None, Order::Descending) + .map(|item| { + let (_, epoch) = item?; + Ok(epoch) + }) + .collect::>>()?; + + println!("EPOCHS: {:?}", all_epochs); + // // Return early if the epoch is the first one // if new_epoch_id == 1 { // // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) @@ -294,7 +306,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result, deps: &DepsMut, - config: white_whale_std::bonding_manager::Config, + config: Config, whale: &mut Coin, distribution_denom: &String, messages: &mut Vec, @@ -171,23 +171,32 @@ pub fn swap_coins_to_main_token( }) .collect(); for coin in coins_to_swap { + println!("Swapping {} to {}", coin.denom, distribution_denom); + let swap_route_query = white_whale_std::pool_manager::QueryMsg::SwapRoute { offer_asset_denom: coin.denom.to_string(), ask_asset_denom: distribution_denom.to_string(), }; + println!("he"); // Query for the routes and pool - let swap_routes: SwapRouteResponse = deps + let swap_routes_response: StdResult = deps .querier - .query_wasm_smart(config.pool_manager_addr.to_string(), &swap_route_query)?; + .query_wasm_smart(config.pool_manager_addr.to_string(), &swap_route_query); + + println!("swap_routes_response: {:?}", swap_routes_response); + let swap_routes = match swap_routes_response { + Ok(swap_routes) => swap_routes, + // no routes, skip + Err(_) => continue, + }; + + // sanity check + if swap_routes.swap_route.swap_operations.is_empty() { + // skip swap if there's not swap route for it + continue; + } - ensure!( - !swap_routes.swap_route.swap_operations.is_empty(), - ContractError::NoSwapRoute { - asset1: coin.denom.to_string(), - asset2: distribution_denom.to_string() - } - ); // check if the pool has any assets, if not skip the swap // Note we are only checking the first operation here. Might be better to another loop to check all operations let pool_query = white_whale_std::pool_manager::QueryMsg::Pool { diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index 67553aee..ef5f4b09 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -51,7 +51,6 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let swap_route_key = SWAP_ROUTES.key((&offer_asset_denom, &ask_asset_denom)); - let swap_operations = swap_route_key .load(deps.storage) diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index 18a6c3af..fc37dfb3 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -63,18 +63,24 @@ fn deposit_and_withdraw_sanity_check() { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_denoms, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); let contract_addr = suite.pool_manager_addr.clone(); let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); @@ -217,21 +223,27 @@ mod pool_creation_failures { }, extra_fees: vec![], }; - // Create a poo - suite.instantiate_default().create_pool( - creator.clone(), - asset_infos, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - None, - vec![coin(90, "uusd")], - |result| { - let err = result.unwrap_err().downcast::().unwrap(); + // Create a pool + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) + .create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + None, + vec![coin(90, "uusd")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); match err { ContractError::InvalidPoolCreationFee { .. } => {} - _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + _ => panic!("Wrong error type, should return ContractError::InvalidPoolCreationFee"), } }, ); @@ -285,6 +297,10 @@ mod pool_creation_failures { // Create a poo suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), asset_infos.clone(), @@ -371,6 +387,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -565,6 +585,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -709,6 +733,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -869,6 +897,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -1098,6 +1130,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -1265,6 +1301,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -1431,6 +1471,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -1629,6 +1673,10 @@ mod router { // Create a pool suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), first_pool, @@ -1849,18 +1897,24 @@ mod swapping { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_infos, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); // Query pool info to ensure the query is working fine suite.query_pool_info("whale-uluna".to_string(), |result| { @@ -2070,18 +2124,24 @@ mod swapping { }; // Create a stableswap pool with amp = 100 - suite.instantiate_default().create_pool( - creator.clone(), - asset_infos, - vec![6u8, 6u8], - pool_fees, - PoolType::StableSwap { amp: 100 }, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8], + pool_fees, + PoolType::StableSwap { amp: 100 }, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); // Lets try to add liquidity suite.provide_liquidity( @@ -2264,18 +2324,24 @@ mod swapping { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_infos, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); // Lets try to add liquidity, 1000 of each token. suite.provide_liquidity( @@ -2554,18 +2620,24 @@ mod locking_lp { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_denoms, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); let contract_addr = suite.pool_manager_addr.clone(); let incentive_manager_addr = suite.incentive_manager_addr.clone(); @@ -2629,7 +2701,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2681,7 +2753,7 @@ mod locking_lp { assert_eq!(positions.len(), 2); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2689,7 +2761,7 @@ mod locking_lp { }); assert_eq!(positions[1], Position { identifier: "2".to_string(), - lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, + lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, unlocking_duration: 200_000, open: true, expiring_at: None, @@ -2744,18 +2816,24 @@ mod locking_lp { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_denoms, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); let contract_addr = suite.pool_manager_addr.clone(); let incentive_manager_addr = suite.incentive_manager_addr.clone(); @@ -2819,7 +2897,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2872,7 +2950,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2941,18 +3019,24 @@ mod provide_liquidity { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_denoms, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); let contract_addr = suite.pool_manager_addr.clone(); let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); @@ -3390,18 +3474,24 @@ mod provide_liquidity { }; // Create a pool - suite.instantiate_default().create_pool( - creator.clone(), - asset_denoms, - vec![6u8, 6u8], - pool_fees, - PoolType::ConstantProduct, - Some("whale-uluna".to_string()), - vec![coin(1000, "uusd")], - |result| { + suite + .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { result.unwrap(); - }, - ); + }) + .create_pool( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + PoolType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); let contract_addr = suite.pool_manager_addr.clone(); @@ -4041,6 +4131,10 @@ mod multiple_pools { // Create pools suite .instantiate_default() + .add_one_day() + .create_new_epoch(|result| { + result.unwrap(); + }) .create_pool( creator.clone(), asset_denoms_1.clone(), @@ -4177,15 +4271,15 @@ mod multiple_pools { balances, vec![ Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { @@ -4232,7 +4326,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1001000, "uwhale"), coin(999070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4275,7 +4369,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_140, "uwhale"), coin(1_001_070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4328,7 +4422,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1001000, "uwhale"), coin(999_150, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4364,7 +4458,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_300, "uwhale"), coin(1_001_150, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4418,7 +4512,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1003000, "uluna"), coin(997_218, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4459,7 +4553,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_001_599, "uluna"), coin(998_718, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4498,15 +4592,15 @@ mod multiple_pools { let balances = result.unwrap(); assert_eq!(balances, vec![ Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { @@ -4556,7 +4650,7 @@ mod multiple_pools { // this should have not changed since last time, since we didn't touch this pool assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_140, "uwhale"), coin(1_001_070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4574,7 +4668,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_004_300, "uwhale"), coin(996_913, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4591,7 +4685,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_005_587, "uluna"), coin(995_035, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4626,15 +4720,15 @@ mod multiple_pools { let balances = result.unwrap(); assert_eq!(balances, vec![ Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 122a0585..ea7c6fce 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use white_whale_std::pool_manager::{ Config, FeatureToggle, PoolInfoResponse, ReverseSimulateSwapOperationsResponse, ReverseSimulationResponse, SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, - SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, + SwapRoute, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; use white_whale_std::pool_manager::{InstantiateMsg, PoolType}; @@ -18,7 +18,6 @@ use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig}; use white_whale_std::fee::PoolFee; use white_whale_std::incentive_manager::PositionsResponse; use white_whale_std::lp_common::LP_SYMBOL; -use white_whale_std::pool_network::asset::AssetInfo; use white_whale_testing::multi_test::stargate_mock::StargateMock; fn contract_pool_manager() -> Box> { @@ -35,11 +34,12 @@ fn contract_pool_manager() -> Box> { /// Creates the whale lair contract pub fn bonding_manager_contract() -> Box> { let contract = ContractWrapper::new( - whale_lair::contract::execute, - whale_lair::contract::instantiate, - whale_lair::contract::query, + bonding_manager::contract::execute, + bonding_manager::contract::instantiate, + bonding_manager::contract::query, ) - .with_migrate(whale_lair::contract::migrate); + .with_reply(bonding_manager::contract::reply) + .with_migrate(bonding_manager::contract::migrate); Box::new(contract) } @@ -104,6 +104,13 @@ impl TestingSuite { self } + pub(crate) fn add_one_day(&mut self) -> &mut Self { + let mut block_info = self.app.block_info(); + block_info.time = block_info.time.plus_days(1); + self.app.set_block(block_info); + + self + } pub(crate) fn get_lp_denom(&self, pool_identifier: String) -> String { format!( @@ -174,10 +181,32 @@ impl TestingSuite { &msg, &[], "mock pool manager", - Some(creator.into_string()), + Some(creator.clone().into_string()), ) .unwrap(); + let bonding_manager_addr = self.bonding_manager_addr.clone(); + + if !bonding_manager_addr.into_string().is_empty() { + let pool_manager_addr = self.pool_manager_addr.clone(); + + let msg = white_whale_std::bonding_manager::ExecuteMsg::UpdateConfig { + owner: None, + pool_manager_addr: Some(pool_manager_addr.into_string()), + unbonding_period: None, + growth_rate: None, + }; + + self.app + .execute_contract( + creator.clone(), + self.bonding_manager_addr.clone(), + &msg, + &[], + ) + .unwrap(); + } + self } @@ -186,6 +215,8 @@ impl TestingSuite { self.create_bonding_manager(); self.create_epoch_manager(); self.create_incentive_manager(); + self.add_hook(self.incentive_manager_addr.clone()); + self.add_hook(self.bonding_manager_addr.clone()); // 25 April 2024 15:00:00 UTC let timestamp = Timestamp::from_seconds(1714057200); @@ -200,19 +231,12 @@ impl TestingSuite { fn create_bonding_manager(&mut self) { let bonding_manager_id = self.app.store_code(bonding_manager_contract()); - // create whale lair - // todo replace with bonding manager InstantiateMsg - let msg = white_whale_std::whale_lair::InstantiateMsg { + let msg = white_whale_std::bonding_manager::InstantiateMsg { + distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(86400u64), growth_rate: Decimal::one(), - bonding_assets: vec![ - AssetInfo::NativeToken { - denom: "bWHALE".to_string(), - }, - AssetInfo::NativeToken { - denom: "ampWHALE".to_string(), - }, - ], + bonding_assets: vec!["bWHALE".to_string(), "ampWHALE".to_string()], + grace_period: Default::default(), }; let creator = self.creator().clone(); @@ -257,6 +281,21 @@ impl TestingSuite { ) .unwrap(); } + + fn add_hook(&mut self, contract: Addr) { + let epoch_manager_id = self.app.store_code(epoch_manager_contract()); + + let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::AddHook { + contract_addr: contract.to_string(), + }; + + let creator = self.creator().clone(); + + self.app + .execute_contract(creator, self.epoch_manager_addr.clone(), &msg, &[]) + .unwrap(); + } + fn create_incentive_manager(&mut self) { let incentive_manager_id = self.app.store_code(incentive_manager_contract()); @@ -478,7 +517,7 @@ impl TestingSuite { pub(crate) fn add_swap_routes( &mut self, sender: Addr, - swap_routes: Vec, + swap_routes: Vec, result: impl Fn(Result), ) -> &mut Self { result(self.app.execute_contract( @@ -491,12 +530,64 @@ impl TestingSuite { self } + /// Adds swap routes to the pool manager contract. + #[track_caller] + pub(crate) fn add_swap_routes_default( + &mut self, + sender: Addr, + result: impl Fn(Result), + ) -> &mut Self { + let swap_routes = vec![ + SwapRoute { + offer_asset_denom: "uluna".to_string(), + ask_asset_denom: "uwhale".to_string(), + swap_operations: vec![ + SwapOperation::WhaleSwap { + token_in_denom: "uwhale".to_string(), + token_out_denom: "uluna".to_string(), + pool_identifier: "whale-uluna".to_string(), + }, + SwapOperation::WhaleSwap { + token_in_denom: "uluna".to_string(), + token_out_denom: "uusd".to_string(), + pool_identifier: "uluna-uusd".to_string(), + }, + ], + }, + SwapRoute { + offer_asset_denom: "uwhale".to_string(), + ask_asset_denom: "uusd".to_string(), + swap_operations: vec![ + SwapOperation::WhaleSwap { + token_in_denom: "uwhale".to_string(), + token_out_denom: "uluna".to_string(), + pool_identifier: "whale-uluna".to_string(), + }, + SwapOperation::WhaleSwap { + token_in_denom: "uluna".to_string(), + token_out_denom: "uusd".to_string(), + pool_identifier: "uluna-uusd".to_string(), + }, + ], + }, + ]; + + result(self.app.execute_contract( + sender, + self.pool_manager_addr.clone(), + &white_whale_std::pool_manager::ExecuteMsg::AddSwapRoutes { swap_routes }, + &[], + )); + + self + } + /// Removes swap routes from the pool manager contract. #[track_caller] pub(crate) fn remove_swap_routes( &mut self, sender: Addr, - swap_routes: Vec, + swap_routes: Vec, result: impl Fn(Result), ) -> &mut Self { result(self.app.execute_contract( @@ -508,6 +599,24 @@ impl TestingSuite { self } + + /// Creates a new epoch. + #[track_caller] + pub(crate) fn create_new_epoch( + &mut self, + result: impl Fn(Result), + ) -> &mut Self { + let user = self.creator(); + + result(self.app.execute_contract( + user, + self.epoch_manager_addr.clone(), + &white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch, + &[], + )); + + self + } } /// queries From db23e50048dd3044f645342cedd188c376880658 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 7 May 2024 15:51:52 +0100 Subject: [PATCH 19/51] chore: remove factory flags from token_factory package --- packages/white-whale-std/src/lib.rs | 12 ++---------- packages/white-whale-std/src/tokenfactory/burn.rs | 12 ------------ packages/white-whale-std/src/tokenfactory/common.rs | 1 - .../src/tokenfactory/create_denom.rs | 13 +------------ 4 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/white-whale-std/src/lib.rs b/packages/white-whale-std/src/lib.rs index 6af2ef3f..d5fc7390 100644 --- a/packages/white-whale-std/src/lib.rs +++ b/packages/white-whale-std/src/lib.rs @@ -1,6 +1,6 @@ -pub mod common; - +pub mod bonding_manager; pub mod coin; +pub mod common; pub mod constants; pub mod epoch_manager; pub mod fee; @@ -12,14 +12,6 @@ pub mod migrate_guards; pub mod pool_manager; pub mod pool_network; pub mod token_factory; - -pub mod bonding_manager; - -#[cfg(any( - feature = "token_factory", - feature = "osmosis_token_factory", - feature = "injective" -))] pub mod tokenfactory; pub mod traits; pub mod vault_manager; diff --git a/packages/white-whale-std/src/tokenfactory/burn.rs b/packages/white-whale-std/src/tokenfactory/burn.rs index d372a25b..b6c58e93 100644 --- a/packages/white-whale-std/src/tokenfactory/burn.rs +++ b/packages/white-whale-std/src/tokenfactory/burn.rs @@ -1,18 +1,6 @@ use std::str::FromStr; -#[cfg(any( - feature = "token_factory", - feature = "osmosis_token_factory", - feature = "injective" -))] use crate::tokenfactory::common::{create_msg, MsgTypes}; -#[allow(unused_imports)] -#[cfg(any( - feature = "token_factory", - feature = "osmosis_token_factory", - feature = "injective" -))] -use crate::tokenfactory::mint::MsgMint; use anybuf::{Anybuf, Bufany}; use cosmwasm_schema::cw_serde; #[allow(unused_imports)] diff --git a/packages/white-whale-std/src/tokenfactory/common.rs b/packages/white-whale-std/src/tokenfactory/common.rs index 9d10cf12..af745aaf 100644 --- a/packages/white-whale-std/src/tokenfactory/common.rs +++ b/packages/white-whale-std/src/tokenfactory/common.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "token_factory")] use cosmwasm_schema::cw_serde; use cosmwasm_std::{CosmosMsg, StdResult}; diff --git a/packages/white-whale-std/src/tokenfactory/create_denom.rs b/packages/white-whale-std/src/tokenfactory/create_denom.rs index 09a26f03..121244e5 100644 --- a/packages/white-whale-std/src/tokenfactory/create_denom.rs +++ b/packages/white-whale-std/src/tokenfactory/create_denom.rs @@ -1,19 +1,8 @@ -/// Returns the MsgCreateDenom Stargate message -#[cfg(any( - feature = "token_factory", - feature = "osmosis_token_factory", - feature = "injective" -))] use crate::tokenfactory::common::{create_msg, MsgTypes}; use anybuf::{Anybuf, Bufany}; use cosmwasm_schema::cw_serde; use cosmwasm_std::StdResult; -/// Returns the MsgCreateDenom Stargate message -#[cfg(any( - feature = "token_factory", - feature = "osmosis_token_factory", - feature = "injective" -))] + use cosmwasm_std::{Addr, CosmosMsg}; use crate::tokenfactory::common::EncodeMessage; From 26472df8a4c548862fbee6edd78795cf624665f4 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 8 May 2024 16:15:23 +0100 Subject: [PATCH 20/51] refactor: bonding-manager --- Cargo.lock | 1 + .../liquidity_hub/bonding-manager/Cargo.toml | 1 + .../schema/bonding-manager.json | 584 ++++++++++-------- .../bonding-manager/schema/raw/execute.json | 166 ++++- .../schema/raw/instantiate.json | 7 +- .../bonding-manager/schema/raw/query.json | 87 ++- .../schema/raw/response_to_bonded.json | 19 +- .../schema/raw/response_to_claimable.json | 1 + .../schema/raw/response_to_config.json | 20 +- .../schema/raw/response_to_ownership.json | 95 +++ .../schema/raw/response_to_total_bonded.json | 19 +- .../schema/raw/response_to_unbonding.json | 8 +- .../schema/raw/response_to_weight.json | 29 +- .../schema/raw/response_to_withdrawable.json | 7 +- .../bonding-manager/src/commands.rs | 286 ++++++--- .../bonding-manager/src/contract.rs | 272 +++----- .../bonding-manager/src/error.rs | 43 +- .../bonding-manager/src/helpers.rs | 142 +++-- .../bonding-manager/src/queries.rs | 215 +++---- .../bonding-manager/src/state.rs | 88 +-- .../bonding-manager/src/tests/claim.rs | 501 +++++++++++---- .../bonding-manager/src/tests/instantiate.rs | 14 +- .../bonding-manager/src/tests/rewards.rs | 67 +- .../bonding-manager/src/tests/robot.rs | 81 ++- .../src/tests/update_config.rs | 8 +- .../epoch-manager/src/commands.rs | 16 +- .../epoch-manager/src/contract.rs | 16 +- .../liquidity_hub/epoch-manager/src/error.rs | 3 + .../liquidity_hub/pool-manager/src/error.rs | 3 - .../pool-manager/src/manager/commands.rs | 9 - .../liquidity_hub/pool-manager/src/queries.rs | 6 +- .../white-whale-std/src/bonding_manager.rs | 91 ++- packages/white-whale-std/src/constants.rs | 2 +- .../src/epoch_manager/common.rs | 4 +- 34 files changed, 1737 insertions(+), 1174 deletions(-) create mode 100644 contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json diff --git a/Cargo.lock b/Cargo.lock index 74c8a406..b11f13a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-ownable", "cw-storage-plus", "cw-utils", "cw2", diff --git a/contracts/liquidity_hub/bonding-manager/Cargo.toml b/contracts/liquidity_hub/bonding-manager/Cargo.toml index 5e913cf3..a00aaa01 100644 --- a/contracts/liquidity_hub/bonding-manager/Cargo.toml +++ b/contracts/liquidity_hub/bonding-manager/Cargo.toml @@ -40,6 +40,7 @@ thiserror.workspace = true white-whale-std.workspace = true cw-utils.workspace = true pool-manager.workspace = true +cw-ownable.workspace = true [dev-dependencies] cw-multi-test.workspace = true diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 63d83d38..bb3a525c 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -9,6 +9,7 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", "unbonding_period" @@ -25,6 +26,10 @@ "description": "Denom to be swapped to and rewarded", "type": "string" }, + "epoch_manager_addr": { + "description": "The epoch manager contract", + "type": "string" + }, "grace_period": { "description": "Grace period the maximum age of a bucket before fees are forwarded from it", "allOf": [ @@ -42,7 +47,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" @@ -68,17 +73,10 @@ "oneOf": [ { "description": "Bonds the specified [Asset].", - "type": "object", - "required": [ + "type": "string", + "enum": [ "bond" - ], - "properties": { - "bond": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Unbonds the specified [Asset].", @@ -94,7 +92,12 @@ ], "properties": { "asset": { - "$ref": "#/definitions/Coin" + "description": "The asset to unbond.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] } }, "additionalProperties": false @@ -116,6 +119,7 @@ ], "properties": { "denom": { + "description": "The denom to withdraw.", "type": "string" } }, @@ -135,6 +139,7 @@ "type": "object", "properties": { "growth_rate": { + "description": "The new growth rate.", "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -144,19 +149,15 @@ } ] }, - "owner": { - "type": [ - "string", - "null" - ] - }, "pool_manager_addr": { + "description": "The new pool manager address.", "type": [ "string", "null" ] }, "unbonding_period": { + "description": "The unbonding period.", "anyOf": [ { "$ref": "#/definitions/Uint64" @@ -173,27 +174,21 @@ "additionalProperties": false }, { - "type": "object", - "required": [ + "description": "Claims the available rewards", + "type": "string", + "enum": [ "claim" - ], - "properties": { - "claim": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Fills the whale lair with new rewards.", + "description": "Fills the contract with new rewards.", "type": "string", "enum": [ "fill_rewards" ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" @@ -206,16 +201,85 @@ ], "properties": { "current_epoch": { - "$ref": "#/definitions/Epoch" + "description": "The current epoch, the one that was newly created.", + "allOf": [ + { + "$ref": "#/definitions/Epoch" + } + ] } }, "additionalProperties": false } }, "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false } ], "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, "Coin": { "type": "object", "required": [ @@ -253,6 +317,53 @@ }, "additionalProperties": false }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -277,17 +388,10 @@ "oneOf": [ { "description": "Returns the [Config] of te contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the amount of assets that have been bonded by the specified address.", @@ -298,12 +402,13 @@ "properties": { "bonded": { "type": "object", - "required": [ - "address" - ], "properties": { "address": { - "type": "string" + "description": "The address to check for bonded assets. If none is provided, all bonded assets in the contract are returned.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false @@ -326,12 +431,15 @@ ], "properties": { "address": { + "description": "The address to check for unbonding assets.", "type": "string" }, "denom": { + "description": "The denom to check for unbonding assets.", "type": "string" }, "limit": { + "description": "The maximum amount of unbonding assets to return.", "type": [ "integer", "null" @@ -340,6 +448,7 @@ "minimum": 0.0 }, "start_after": { + "description": "The amount of unbonding assets to skip. Allows pagination.", "type": [ "integer", "null" @@ -368,9 +477,11 @@ ], "properties": { "address": { + "description": "The address to check for withdrawable assets.", "type": "string" }, "denom": { + "description": "The denom to check for withdrawable assets.", "type": "string" } }, @@ -393,9 +504,11 @@ ], "properties": { "address": { + "description": "The address to check for weight.", "type": "string" }, "global_index": { + "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ { "$ref": "#/definitions/GlobalIndex" @@ -406,6 +519,7 @@ ] }, "timestamp": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", "anyOf": [ { "$ref": "#/definitions/Timestamp" @@ -421,65 +535,45 @@ }, "additionalProperties": false }, - { - "description": "Returns the total amount of assets that have been bonded to the contract.", - "type": "object", - "required": [ - "total_bonded" - ], - "properties": { - "total_bonded": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Returns the global index of the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "global_index" - ], - "properties": { - "global_index": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Returns the [Epoch]s that can be claimed.", + "description": "Returns the [Epoch]s that can be claimed by an address.", "type": "object", "required": [ - "claimable_epochs" + "claimable" ], "properties": { - "claimable_epochs": { + "claimable": { "type": "object", + "properties": { + "address": { + "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Query the contract's ownership information", "type": "object", "required": [ - "claimable" + "ownership" ], "properties": { - "claimable": { + "ownership": { "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, "additionalProperties": false } }, @@ -578,63 +672,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" } - } - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "claimable": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ClaimableEpochsResponse", - "type": "object", - "required": [ - "epochs" - ], - "properties": { - "epochs": { - "type": "array", - "items": { - "$ref": "#/definitions/Epoch" - } + ] } }, "additionalProperties": false, @@ -654,98 +719,6 @@ } } }, - "Epoch": { - "type": "object", - "required": [ - "available", - "claimed", - "global_index", - "id", - "start_time", - "total" - ], - "properties": { - "available": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "claimed": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "global_index": { - "$ref": "#/definitions/GlobalIndex" - }, - "id": { - "$ref": "#/definitions/Uint64" - }, - "start_time": { - "$ref": "#/definitions/Timestamp" - }, - "total": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - }, - "GlobalIndex": { - "type": "object", - "required": [ - "bonded_amount", - "bonded_assets", - "timestamp", - "weight" - ], - "properties": { - "bonded_amount": { - "description": "The total amount of tokens bonded in the contract.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "bonded_assets": { - "description": "Assets that are bonded in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] - }, - "weight": { - "description": "The total weight of the bond at the given block height.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -756,7 +729,7 @@ } } }, - "claimable_epochs": { + "claimable": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimableEpochsResponse", "type": "object", @@ -765,6 +738,7 @@ ], "properties": { "epochs": { + "description": "The epochs that can be claimed by the address.", "type": "array", "items": { "$ref": "#/definitions/Epoch" @@ -897,9 +871,9 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", - "owner", "pool_manager_addr", "unbonding_period" ], @@ -915,6 +889,14 @@ "description": "Distribution denom for the rewards", "type": "string" }, + "epoch_manager_addr": { + "description": "Epoch Manager contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "grace_period": { "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", "allOf": [ @@ -931,14 +913,6 @@ } ] }, - "owner": { - "description": "Owner of the contract.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, "pool_manager_addr": { "description": "Pool Manager contract address for swapping", "allOf": [ @@ -948,7 +922,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" @@ -1050,50 +1024,94 @@ } } }, - "total_bonded": { + "ownership": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BondedResponse", - "description": "Response for the Bonded query", + "title": "Ownership_for_String", + "description": "The contract's ownership info", "type": "object", - "required": [ - "bonded_assets", - "first_bonded_epoch_id", - "total_bonded" - ], "properties": { - "bonded_assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] }, - "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] }, - "total_bonded": { - "$ref": "#/definitions/Uint128" + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false, "definitions": { - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false }, - "denom": { - "type": "string" + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } - } + ] }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] }, "Uint64": { "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", @@ -1112,9 +1130,15 @@ ], "properties": { "total_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of unbonded tokens by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "unbonding_requests": { + "description": "The total amount of unbonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Bond" @@ -1205,19 +1229,40 @@ ], "properties": { "address": { + "description": "The weight of the address.", "type": "string" }, "global_weight": { - "$ref": "#/definitions/Uint128" + "description": "The global weight of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "share": { - "$ref": "#/definitions/Decimal" + "description": "The share the address has of the rewards at the particular timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "timestamp": { - "$ref": "#/definitions/Timestamp" + "description": "The timestamp at which the weight was calculated.", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "weight": { - "$ref": "#/definitions/Uint128" + "description": "The weight of the address at the given timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, @@ -1254,7 +1299,12 @@ ], "properties": { "withdrawable_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of withdrawable assets by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index bbc26076..9f165688 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -4,17 +4,10 @@ "oneOf": [ { "description": "Bonds the specified [Asset].", - "type": "object", - "required": [ + "type": "string", + "enum": [ "bond" - ], - "properties": { - "bond": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Unbonds the specified [Asset].", @@ -30,7 +23,12 @@ ], "properties": { "asset": { - "$ref": "#/definitions/Coin" + "description": "The asset to unbond.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] } }, "additionalProperties": false @@ -52,6 +50,7 @@ ], "properties": { "denom": { + "description": "The denom to withdraw.", "type": "string" } }, @@ -71,6 +70,7 @@ "type": "object", "properties": { "growth_rate": { + "description": "The new growth rate.", "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -80,19 +80,15 @@ } ] }, - "owner": { - "type": [ - "string", - "null" - ] - }, "pool_manager_addr": { + "description": "The new pool manager address.", "type": [ "string", "null" ] }, "unbonding_period": { + "description": "The unbonding period.", "anyOf": [ { "$ref": "#/definitions/Uint64" @@ -109,27 +105,21 @@ "additionalProperties": false }, { - "type": "object", - "required": [ + "description": "Claims the available rewards", + "type": "string", + "enum": [ "claim" - ], - "properties": { - "claim": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Fills the whale lair with new rewards.", + "description": "Fills the contract with new rewards.", "type": "string", "enum": [ "fill_rewards" ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" @@ -142,16 +132,85 @@ ], "properties": { "current_epoch": { - "$ref": "#/definitions/Epoch" + "description": "The current epoch, the one that was newly created.", + "allOf": [ + { + "$ref": "#/definitions/Epoch" + } + ] } }, "additionalProperties": false } }, "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false } ], "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, "Coin": { "type": "object", "required": [ @@ -189,6 +248,53 @@ }, "additionalProperties": false }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json index 3ffb9879..b0d74ffa 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json @@ -5,6 +5,7 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", "unbonding_period" @@ -21,6 +22,10 @@ "description": "Denom to be swapped to and rewarded", "type": "string" }, + "epoch_manager_addr": { + "description": "The epoch manager contract", + "type": "string" + }, "grace_period": { "description": "Grace period the maximum age of a bucket before fees are forwarded from it", "allOf": [ @@ -38,7 +43,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index c016c928..096226bb 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -4,17 +4,10 @@ "oneOf": [ { "description": "Returns the [Config] of te contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the amount of assets that have been bonded by the specified address.", @@ -25,12 +18,13 @@ "properties": { "bonded": { "type": "object", - "required": [ - "address" - ], "properties": { "address": { - "type": "string" + "description": "The address to check for bonded assets. If none is provided, all bonded assets in the contract are returned.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false @@ -53,12 +47,15 @@ ], "properties": { "address": { + "description": "The address to check for unbonding assets.", "type": "string" }, "denom": { + "description": "The denom to check for unbonding assets.", "type": "string" }, "limit": { + "description": "The maximum amount of unbonding assets to return.", "type": [ "integer", "null" @@ -67,6 +64,7 @@ "minimum": 0.0 }, "start_after": { + "description": "The amount of unbonding assets to skip. Allows pagination.", "type": [ "integer", "null" @@ -95,9 +93,11 @@ ], "properties": { "address": { + "description": "The address to check for withdrawable assets.", "type": "string" }, "denom": { + "description": "The denom to check for withdrawable assets.", "type": "string" } }, @@ -120,9 +120,11 @@ ], "properties": { "address": { + "description": "The address to check for weight.", "type": "string" }, "global_index": { + "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ { "$ref": "#/definitions/GlobalIndex" @@ -133,6 +135,7 @@ ] }, "timestamp": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", "anyOf": [ { "$ref": "#/definitions/Timestamp" @@ -148,65 +151,45 @@ }, "additionalProperties": false }, - { - "description": "Returns the total amount of assets that have been bonded to the contract.", - "type": "object", - "required": [ - "total_bonded" - ], - "properties": { - "total_bonded": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Returns the global index of the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "global_index" - ], - "properties": { - "global_index": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Returns the [Epoch]s that can be claimed.", + "description": "Returns the [Epoch]s that can be claimed by an address.", "type": "object", "required": [ - "claimable_epochs" + "claimable" ], "properties": { - "claimable_epochs": { + "claimable": { "type": "object", + "properties": { + "address": { + "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Query the contract's ownership information", "type": "object", "required": [ - "claimable" + "ownership" ], "properties": { - "claimable": { + "ownership": { "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, "additionalProperties": false } }, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json index 5b176215..d8a0ddbd 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json @@ -5,21 +5,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, 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 5084901f..6a728ff2 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 @@ -7,6 +7,7 @@ ], "properties": { "epochs": { + "description": "The epochs that can be claimed by the address.", "type": "array", "items": { "$ref": "#/definitions/Epoch" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json index 2ef8b2bc..3d6850fc 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json @@ -5,9 +5,9 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", - "owner", "pool_manager_addr", "unbonding_period" ], @@ -23,6 +23,14 @@ "description": "Distribution denom for the rewards", "type": "string" }, + "epoch_manager_addr": { + "description": "Epoch Manager contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "grace_period": { "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", "allOf": [ @@ -39,14 +47,6 @@ } ] }, - "owner": { - "description": "Owner of the contract.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, "pool_manager_addr": { "description": "Pool Manager contract address for swapping", "allOf": [ @@ -56,7 +56,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json new file mode 100644 index 00000000..afe1713f --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json @@ -0,0 +1,95 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json index 5b176215..d8a0ddbd 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json @@ -5,21 +5,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, 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 29decae8..476b888b 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 @@ -9,9 +9,15 @@ ], "properties": { "total_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of unbonded tokens by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "unbonding_requests": { + "description": "The total amount of unbonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Bond" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json index 4355e1f9..0e017e02 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json @@ -12,19 +12,40 @@ ], "properties": { "address": { + "description": "The weight of the address.", "type": "string" }, "global_weight": { - "$ref": "#/definitions/Uint128" + "description": "The global weight of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "share": { - "$ref": "#/definitions/Decimal" + "description": "The share the address has of the rewards at the particular timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "timestamp": { - "$ref": "#/definitions/Timestamp" + "description": "The timestamp at which the weight was calculated.", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "weight": { - "$ref": "#/definitions/Uint128" + "description": "The weight of the address at the given timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json index 79b3317c..42012032 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json @@ -8,7 +8,12 @@ ], "properties": { "withdrawable_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of withdrawable assets by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 25333f17..2f6457fa 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -3,11 +3,11 @@ use cosmwasm_std::{ StdError, StdResult, SubMsg, Timestamp, Uint128, Uint64, }; -use white_whale_std::bonding_manager::Bond; +use white_whale_std::bonding_manager::{Bond, Epoch, GlobalIndex}; use white_whale_std::pool_network::asset; use crate::helpers::validate_growth_rate; -use crate::queries::{get_current_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; +use crate::queries::{get_expiring_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; use crate::state::{ update_global_weight, update_local_weight, BOND, CONFIG, EPOCHS, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, @@ -22,8 +22,10 @@ pub(crate) fn bond( env: Env, asset: Coin, ) -> Result { + println!("bonding"); helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps, &env)?; + println!("bonding 2"); let mut bond = BOND .key((&info.sender, &asset.denom)) .may_load(deps.storage)? @@ -37,15 +39,12 @@ pub(crate) fn bond( // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; - // let new_bond_weight = get_weight(timestamp, bond.weight, asset.amount, config.growth_rate, bond.timestamp)?; bond.weight = bond.weight.checked_add(asset.amount)?; bond = update_local_weight(&mut deps, info.sender.clone(), timestamp, bond)?; BOND.save(deps.storage, (&info.sender, &asset.denom), &bond)?; // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - // global_index = update_global_weight(&mut deps, timestamp, global_index)?; - // move into one common func TODO: // include time term in the weight global_index.weight = global_index.weight.checked_add(asset.amount)?; global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; @@ -55,17 +54,6 @@ pub(crate) fn bond( GLOBAL.save(deps.storage, &global_index)?; - let epoch = get_current_epoch(deps.as_ref())?.epoch; - EPOCHS.update( - deps.storage, - &epoch.id.to_be_bytes(), - |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.global_index = global_index.clone(); - Ok(bucket) - }, - )?; - Ok(Response::default().add_attributes(vec![ ("action", "bond".to_string()), ("address", info.sender.to_string()), @@ -83,7 +71,7 @@ pub(crate) fn unbond( ) -> Result { ensure!( asset.amount > Uint128::zero(), - ContractError::InvalidUnbondingAmount {} + ContractError::InvalidUnbondingAmount ); helpers::validate_claimed(&deps, &info)?; @@ -95,14 +83,14 @@ pub(crate) fn unbond( // check if the address has enough bond ensure!( unbond.asset.amount >= asset.amount, - ContractError::InsufficientBond {} + ContractError::InsufficientBond ); // update local values, decrease the bond unbond = update_local_weight(&mut deps, info.sender.clone(), timestamp, unbond.clone())?; let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); - unbond.weight = unbond.weight.checked_sub(weight_slash)?; - unbond.asset.amount = unbond.asset.amount.checked_sub(asset.amount)?; + unbond.weight = unbond.weight.saturating_sub(weight_slash); + unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); if unbond.asset.amount.is_zero() { BOND.remove(deps.storage, (&info.sender, &asset.denom)); @@ -119,14 +107,13 @@ pub(crate) fn unbond( timestamp, }, )?; - // move this to a function to be reused // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); global_index = update_global_weight(&mut deps, timestamp, global_index)?; - global_index.bonded_amount = global_index.bonded_amount.checked_sub(asset.amount)?; + global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); global_index.bonded_assets = white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index.weight = global_index.weight.checked_sub(weight_slash)?; + global_index.weight = global_index.weight.saturating_sub(weight_slash); GLOBAL.save(deps.storage, &global_index)?; @@ -136,7 +123,7 @@ pub(crate) fn unbond( ("asset", asset.to_string()), ])) } else { - Err(ContractError::NothingToUnbond {}) + Err(ContractError::NothingToUnbond) } } @@ -157,7 +144,7 @@ pub(crate) fn withdraw( let mut refund_amount = Uint128::zero(); - ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw {}); + ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); for unbonding in unbondings { let (ts, bond) = unbonding; @@ -192,25 +179,19 @@ pub(crate) fn withdraw( pub(crate) fn update_config( deps: DepsMut, info: MessageInfo, - owner: Option, pool_manager_addr: Option, unbonding_period: Option, growth_rate: Option, ) -> Result { // check the owner is the one who sent the message + cw_ownable::assert_owner(deps.storage, &info.sender)?; + let mut config = CONFIG.load(deps.storage)?; - if config.owner != info.sender { - return Err(ContractError::Unauthorized {}); - } if let Some(pool_manager_addr) = pool_manager_addr { config.pool_manager_addr = deps.api.addr_validate(&pool_manager_addr)?; } - if let Some(owner) = owner { - config.owner = deps.api.addr_validate(&owner)?; - } - if let Some(unbonding_period) = unbonding_period { config.unbonding_period = unbonding_period; } @@ -224,7 +205,6 @@ pub(crate) fn update_config( Ok(Response::default().add_attributes(vec![ ("action", "update_config".to_string()), - ("owner", config.owner.to_string()), ("pool_manager_addr", config.pool_manager_addr.to_string()), ("unbonding_period", config.unbonding_period.to_string()), ("growth_rate", config.growth_rate.to_string()), @@ -232,46 +212,64 @@ pub(crate) fn update_config( } /// Claims pending rewards for the sender. -pub fn claim(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { - let claimable_epochs = query_claimable(deps.as_ref(), &info.sender)?.epochs; +pub fn claim(deps: DepsMut, info: MessageInfo) -> Result { + let claimable_epochs_for_user = + query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.epochs; ensure!( - !claimable_epochs.is_empty(), - ContractError::NothingToClaim {} + !claimable_epochs_for_user.is_empty(), + ContractError::NothingToClaim ); - let _global = GLOBAL.load(deps.storage)?; + let mut claimable_fees = vec![]; - for mut epoch in claimable_epochs.clone() { - let bonding_weight_response = query_weight( + let mut attributes = vec![]; + for mut epoch in claimable_epochs_for_user.clone() { + let bonding_weight_response_for_epoch = query_weight( deps.as_ref(), epoch.start_time, info.sender.to_string(), Some(epoch.global_index.clone()), )?; + // if the user has no share in the epoch, skip it + if bonding_weight_response_for_epoch.share.is_zero() { + continue; + }; + + // sanity check + ensure!( + bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), + ContractError::InvalidShare + ); + for fee in epoch.total.iter() { - let reward = fee.amount * bonding_weight_response.share; + let reward = fee.amount * bonding_weight_response_for_epoch.share; - if reward.is_zero() { - // nothing to claim - continue; - } // make sure the reward is sound - let _ = epoch + let reward_validation: Result<(), StdError> = epoch .available .iter() .find(|available_fee| available_fee.denom == fee.denom) .map(|available_fee| { if reward > available_fee.amount { - //todo maybe we can just skip this epoch and log something on the attributes instead - // of returning an error and blocking the whole operation - // this would "solve" the case when users unbond and then those who have not claimed - // past epochs won't be able to do it as their rewards exceed the available claimable fees - // cuz their weight increased in relation to the global weight - return Err(ContractError::InvalidReward {}); + attributes.push(( + "error", + ContractError::InvalidReward { + reward, + available: available_fee.amount, + } + .to_string(), + )); } Ok(()) }) - .ok_or_else(|| StdError::generic_err("Invalid fee"))?; + .ok_or(StdError::generic_err("Invalid fee"))?; + + // if the reward is invalid, skip the epoch + match reward_validation { + Ok(_) => {} + Err(_) => continue, + } + let denom = &fee.denom; // add the reward to the claimable fees claimable_fees = asset::aggregate_coins( @@ -285,7 +283,7 @@ pub fn claim(deps: DepsMut, _env: Env, info: MessageInfo) -> Result Result Result Result { + println!( + "EPOCHS: {:?}", + EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .collect::>() + ); + // Finding the most recent EpochID - let most_recent_epoch_id = match EPOCHS + let upcoming_epoch_id = match EPOCHS .keys(deps.storage, None, None, Order::Descending) .next() { Some(epoch_id) => epoch_id?, - None => return Err(ContractError::Unauthorized {}), + None => return Err(ContractError::Unauthorized), }; let config = CONFIG.load(deps.storage)?; @@ -354,7 +365,8 @@ pub(crate) fn fill_rewards( }) .to_owned(); - // coins that are laying in the contract and have not been swapped before for lack of swap routes + // coins (not the distribution_denom) that are laying in the contract and have not been swapped before for lack + // of swap routes let remanent_coins = deps .querier .query_all_balances(env.contract.address)? @@ -365,7 +377,7 @@ pub(crate) fn fill_rewards( println!("remanent_coins: {:?}", remanent_coins); // Each of these helpers will add messages to the messages vector // and may increment the whale Coin above with the result of the swaps - helpers::handle_lp_tokens(&info, &config, &mut submessages)?; + helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; helpers::swap_coins_to_main_token( remanent_coins, &deps, @@ -374,22 +386,132 @@ pub(crate) fn fill_rewards( &distribution_denom, &mut messages, )?; + + println!("here"); // Add the whale to the funds, the whale figure now should be the result // of all the LP token withdrawals and swaps // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned // If this became an issue would could look at replys instead of the query - EPOCHS.update( - deps.storage, - &most_recent_epoch_id, - |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - }, - )?; + EPOCHS.update(deps.storage, &upcoming_epoch_id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; + bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; + Ok(bucket) + })?; Ok(Response::default() .add_messages(messages) .add_submessages(submessages) .add_attributes(vec![("action", "fill_rewards".to_string())])) } + +pub(crate) fn on_epoch_created( + deps: DepsMut, + env: Env, + info: MessageInfo, + current_epoch: white_whale_std::epoch_manager::epoch_manager::Epoch, +) -> Result { + cw_utils::nonpayable(&info)?; + + println!("EpochChangedHook: {:?}", current_epoch); + // A new epoch has been created, update rewards bucket and forward the expiring epoch + // Store epoch and verify the sender is the epoch manager + let config = CONFIG.load(deps.storage)?; + ensure!( + info.sender == config.epoch_manager_addr, + ContractError::Unauthorized + ); + + let global = GLOBAL.may_load(deps.storage)?; + // This happens only on the very first epoch where Global has not been initialised yet + if global.is_none() { + let initial_global_index = GlobalIndex { + timestamp: env.block.time, + ..Default::default() + }; + GLOBAL.save(deps.storage, &initial_global_index)?; + EPOCHS.save( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + &Epoch { + id: current_epoch.id.into(), + start_time: current_epoch.start_time, + global_index: initial_global_index, + ..Epoch::default() + }, + )?; + } + + let global = GLOBAL.load(deps.storage)?; + + // update the global index for the current epoch, take the current snapshot of the global index + EPOCHS.update( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.global_index = global; + Ok(epoch) + }, + )?; + + // todo to delete once the testing is done + let all_epochs: Vec = EPOCHS + .range(deps.storage, None, None, Order::Descending) + .map(|item| { + let (_, epoch) = item?; + Ok(epoch) + }) + .collect::>>()?; + + println!("EPOCHS: {:?}", all_epochs); + + // forward fees from the expiring epoch to the new one. + let mut expiring_epoch = get_expiring_epoch(deps.as_ref())?; + if let Some(expiring_epoch) = expiring_epoch.as_mut() { + // Load all the available assets from the expiring epoch + let amount_to_be_forwarded = EPOCHS + .load(deps.storage, &expiring_epoch.id.to_be_bytes())? + .available; + EPOCHS.update( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.available = + asset::aggregate_coins(epoch.available, amount_to_be_forwarded.clone())?; + epoch.total = asset::aggregate_coins(epoch.total, amount_to_be_forwarded)?; + + Ok(epoch) + }, + )?; + // Set the available assets for the expiring epoch to an empty vec now that they have been + // forwarded + EPOCHS.update( + deps.storage, + &expiring_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.available = vec![]; + Ok(epoch) + }, + )?; + } + + // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in + // the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + let next_epoch_id = Uint64::new(current_epoch.id).checked_add(Uint64::one())?; + EPOCHS.save( + deps.storage, + &next_epoch_id.u64().to_be_bytes(), + &Epoch { + id: next_epoch_id, + start_time: current_epoch.start_time.plus_days(1), + // this global index is to be updated the next time this hook is called, as this future epoch + // will become the current one + global_index: Default::default(), + ..Epoch::default() + }, + )?; + + Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index bc30b780..4cb8fc86 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -1,17 +1,14 @@ -use cosmwasm_std::{entry_point, from_json, Addr, Coin, Order, Reply, Uint128}; +use cosmwasm_std::{ensure, entry_point, from_json, Addr, Coin, Order, Reply, Uint128}; use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::{get_contract_version, set_contract_version}; use cw_utils::parse_reply_execute_data; use white_whale_std::pool_network::asset; -use white_whale_std::bonding_manager::{ - Config, Epoch, ExecuteMsg, GlobalIndex, InstantiateMsg, MigrateMsg, QueryMsg, -}; +use white_whale_std::bonding_manager::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; -use crate::queries::get_expiring_epoch; -use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, EPOCHS, GLOBAL}; +use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, EPOCHS}; use crate::{commands, queries}; // version info for migration info @@ -27,20 +24,17 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - if msg.bonding_assets.len() > BONDING_ASSETS_LIMIT { - return Err(ContractError::InvalidBondingAssetsLimit( - BONDING_ASSETS_LIMIT, - msg.bonding_assets.len(), - )); - } + ensure!( + msg.bonding_assets.len() <= BONDING_ASSETS_LIMIT, + ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, msg.bonding_assets.len(),) + ); validate_growth_rate(msg.growth_rate)?; - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let config = Config { - owner: deps.api.addr_validate(info.sender.as_str())?, pool_manager_addr: Addr::unchecked(""), + epoch_manager_addr: Addr::unchecked(""), distribution_denom: msg.distribution_denom, unbonding_period: msg.unbonding_period, growth_rate: msg.growth_rate, @@ -49,21 +43,11 @@ pub fn instantiate( }; CONFIG.save(deps.storage, &config)?; - // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // Add a new rewards bucket for the new epoch - // EPOCHS.save( - // deps.storage, - // &0u64.to_be_bytes(), - // &Epoch { - // id: 0u64.into(), - // start_time: env.block.time, - // ..Epoch::default() - // }, - // )?; - // GLOBAL.save(deps.storage, &GlobalIndex{ bonded_amount: Uint128::zero(), bonded_assets: vec![], timestamp: env.block.time, weight: Uint128::zero() })?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), - ("owner", config.owner.to_string()), + ("owner", info.sender.to_string()), ("unbonding_period", config.unbonding_period.to_string()), ("growth_rate", config.growth_rate.to_string()), ("bonding_assets", msg.bonding_assets.join(", ")), @@ -79,7 +63,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Bond {} => { + ExecuteMsg::Bond => { let asset_to_bond = helpers::validate_funds(&deps, &info)?; commands::bond(deps, env.block.time, info, env, asset_to_bond) } @@ -92,154 +76,44 @@ pub fn execute( commands::withdraw(deps, env.block.time, info.sender, denom) } ExecuteMsg::UpdateConfig { - owner, pool_manager_addr, unbonding_period, growth_rate, - } => commands::update_config( - deps, - info, - owner, - pool_manager_addr, - unbonding_period, - growth_rate, - ), + } => { + cw_utils::nonpayable(&info)?; + commands::update_config(deps, info, pool_manager_addr, unbonding_period, growth_rate) + } ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), - ExecuteMsg::Claim { .. } => commands::claim(deps, env, info), + ExecuteMsg::Claim => commands::claim(deps, info), ExecuteMsg::EpochChangedHook { current_epoch } => { - println!("EpochChangedHook: {:?}", current_epoch); - // Epoch has been updated, update rewards bucket - // and forward the expiring epoch - // Store epoch manager and verify the sender is him - let global = GLOBAL.may_load(deps.storage)?; - // This happens only on the first epoch where Global has not been initialised yet - if global.is_none() { - let default_global = GlobalIndex { - timestamp: env.block.time, - ..Default::default() - }; - GLOBAL.save(deps.storage, &default_global)?; - EPOCHS.save( - deps.storage, - ¤t_epoch.id.to_be_bytes(), - &Epoch { - id: current_epoch.id.into(), - start_time: current_epoch.start_time, - global_index: default_global, - ..Epoch::default() - }, - )?; - } - let global = GLOBAL.load(deps.storage)?; - - // Review, what if current_epoch form the hook is actually next_epoch_id and then epoch - 1 would be previous one - let new_epoch_id = current_epoch.id; - let next_epoch_id = match new_epoch_id.checked_add(1u64) { - Some(next_epoch_id) => next_epoch_id, - None => return Err(ContractError::Unauthorized {}), - }; - // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // Add a new rewards bucket for the new epoch - EPOCHS.save( - deps.storage, - &next_epoch_id.to_be_bytes(), - &Epoch { - id: next_epoch_id.into(), - start_time: current_epoch.start_time.plus_days(1), - global_index: global, - ..Epoch::default() - }, - )?; - - let all_epochs: Vec = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .map(|item| { - let (_, epoch) = item?; - Ok(epoch) - }) - .collect::>>()?; - - println!("EPOCHS: {:?}", all_epochs); - - // // Return early if the epoch is the first one - // if new_epoch_id == 1 { - // // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // // Add a new rewards bucket for the new epoch - // EPOCHS.save( - // deps.storage, - // &new_epoch_id.to_be_bytes(), - // &Epoch { - // id: next_epoch_id.into(), - // start_time: current_epoch.start_time, - // global_index: global.clone(), - // ..Epoch::default() - // }, - // )?; - // return Ok(Response::default() - // .add_attributes(vec![("action", "epoch_changed_hook".to_string())])); - // } - - // forward fees from the expiring epoch to the new one. - let mut expiring_epoch = get_expiring_epoch(deps.as_ref())?; - if let Some(expiring_epoch) = expiring_epoch.as_mut() { - // Load all the available assets from the expiring epoch - let amount_to_be_forwarded = EPOCHS - .load(deps.storage, &expiring_epoch.id.to_be_bytes())? - .available; - EPOCHS.update( - deps.storage, - &new_epoch_id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = asset::aggregate_coins( - epoch.available, - amount_to_be_forwarded.clone(), - )?; - epoch.total = asset::aggregate_coins(epoch.total, amount_to_be_forwarded)?; - - Ok(epoch) - }, - )?; - // Set the available assets for the expiring epoch to an empty vec now that they have been forwarded - EPOCHS.update( - deps.storage, - &expiring_epoch.id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = vec![]; - Ok(epoch) - }, - )?; - } - - Ok(Response::default() - .add_attributes(vec![("action", "epoch_changed_hook".to_string())])) + commands::on_epoch_created(deps, env, info, current_epoch) + } + ExecuteMsg::UpdateOwnership(action) => { + cw_utils::nonpayable(&info)?; + white_whale_std::common::update_ownership(deps, env, info, action).map_err(Into::into) } } } #[entry_point] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::Config {} => to_json_binary(&queries::query_config(deps)?), - QueryMsg::Bonded { address } => to_json_binary(&queries::query_bonded(deps, address)?), + QueryMsg::Config => Ok(to_json_binary(&queries::query_config(deps)?)?), + QueryMsg::Bonded { address } => Ok(to_json_binary(&queries::query_bonded(deps, address)?)?), QueryMsg::Unbonding { address, denom, start_after, limit, - } => to_json_binary(&queries::query_unbonding( + } => Ok(to_json_binary(&queries::query_unbonding( deps, address, denom, start_after, limit, - )?), - QueryMsg::Withdrawable { address, denom } => to_json_binary(&queries::query_withdrawable( - deps, - env.block.time, - address, - denom, + )?)?), + QueryMsg::Withdrawable { address, denom } => Ok(to_json_binary( + &queries::query_withdrawable(deps, env.block.time, address, denom)?, )?), QueryMsg::Weight { address, @@ -250,20 +124,18 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let timestamp = timestamp.unwrap_or(env.block.time); // TODO: Make better timestamp handling - to_json_binary(&queries::query_weight( + Ok(to_json_binary(&queries::query_weight( deps, timestamp, address, global_index, - )?) + )?)?) } - QueryMsg::TotalBonded {} => to_json_binary(&queries::query_total_bonded(deps)?), - QueryMsg::GlobalIndex {} => to_json_binary(&queries::query_global_index(deps)?), - QueryMsg::Claimable { addr } => to_json_binary(&queries::query_claimable( - deps, - &deps.api.addr_validate(&addr)?, - )?), - QueryMsg::ClaimableEpochs {} => to_json_binary(&queries::get_claimable_epochs(deps)?), + QueryMsg::GlobalIndex => Ok(to_json_binary(&queries::query_global_index(deps)?)?), + QueryMsg::Claimable { address } => { + Ok(to_json_binary(&queries::query_claimable(deps, address)?)?) + } + QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), } } @@ -276,35 +148,16 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result = from_json(data.as_slice())?; let config = CONFIG.load(deps.storage)?; let distribution_denom = config.distribution_denom.clone(); let mut messages = vec![]; - // // Loop msg events to find the transfer event and the assets received - // for event in msg.result.unwrap().events { - // if event.ty == "transfer" { - // let attributes = event.attributes; - // for attr in attributes { - // if attr.key == "amount" { - // let amount_str = attr.value; - // let amounts: Vec<&str> = amount_str.split(',').collect(); - // println!("Amounts: {:?}", amounts); - // for amount in amounts { - // // XXXXucoin is the format at this point, pass it to from_str to get the Coin struct - // coins.push(Coin::from_str(amount).unwrap()); - // } - // } - // } - // } - // } - - // Instead of going over events - // - // Search received coins funds for the distribution denom - let mut whale = coins + // Search received coins funds for the coin that is not the distribution denom + // This will be swapped for + let mut to_be_distribution_asset = coins .iter() .find(|coin| coin.denom.ne(distribution_denom.as_str())) .unwrap_or(&Coin { @@ -312,35 +165,48 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result epoch_id?, - None => return Err(ContractError::Unauthorized {}), - }; - EPOCHS.update(deps.storage, &next_epoch_id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - })?; + + // if the swap was successful and the to_be_distribution_asset.denom is the + // distribution_denom, update the upcoming epoch with the new funds + if to_be_distribution_asset.denom == distribution_denom { + // Finding the upcoming EpochID + let upcoming_epoch_id = match EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .next() + { + Some(epoch_id) => epoch_id?, + None => return Err(ContractError::Unauthorized), + }; + + EPOCHS.update(deps.storage, &upcoming_epoch_id, |epoch| -> StdResult<_> { + let mut upcoming_epoch = epoch.unwrap_or_default(); + upcoming_epoch.available = asset::aggregate_coins( + upcoming_epoch.available, + vec![to_be_distribution_asset.clone()], + )?; + upcoming_epoch.total = asset::aggregate_coins( + upcoming_epoch.total, + vec![to_be_distribution_asset.clone()], + )?; + Ok(upcoming_epoch) + })?; + } Ok(Response::new() .add_messages(messages) .add_attribute("total_withdrawn", msg.id.to_string())) } - _ => Err(ContractError::Unauthorized {}), + _ => Err(ContractError::Unauthorized), } } diff --git a/contracts/liquidity_hub/bonding-manager/src/error.rs b/contracts/liquidity_hub/bonding-manager/src/error.rs index e8685266..9a44ea25 100644 --- a/contracts/liquidity_hub/bonding-manager/src/error.rs +++ b/contracts/liquidity_hub/bonding-manager/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; +use cosmwasm_std::{DivideByZeroError, OverflowError, StdError, Uint128}; +use cw_ownable::OwnershipError; use cw_utils::PaymentError; use semver::Version; use thiserror::Error; @@ -9,7 +10,10 @@ pub enum ContractError { Std(#[from] StdError), #[error("Unauthorized")] - Unauthorized {}, + Unauthorized, + + #[error("{0}")] + OwnershipError(#[from] OwnershipError), #[error("{0}")] PaymentError(#[from] PaymentError), @@ -18,13 +22,13 @@ pub enum ContractError { SemVer(String), #[error("The asset sent doesn't match the asset expected. Please check the denom and amount.")] - AssetMismatch {}, + AssetMismatch, #[error("The amount of tokens to unbond is greater than the amount of tokens bonded.")] - InsufficientBond {}, + InsufficientBond, #[error("The amount of tokens to unbond must be greater than zero.")] - InvalidUnbondingAmount {}, + InvalidUnbondingAmount, #[error("{0}")] DivideByZeroError(#[from] DivideByZeroError), @@ -33,21 +37,18 @@ pub enum ContractError { OverflowError(#[from] OverflowError), #[error("The growth rate must be between 0 and 1. i.e. 0.5 for 50%")] - InvalidGrowthRate {}, + InvalidGrowthRate, #[error( "The amount of bonding assets is greater than the limit allowed. Limit is {0}, sent {1}." )] InvalidBondingAssetsLimit(usize, usize), - #[error("Can only bond native assets.")] - InvalidBondingAsset {}, - - #[error("Nothing to unbond.")] - NothingToUnbond {}, + #[error("Nothing to unbond")] + NothingToUnbond, - #[error("Nothing to withdraw.")] - NothingToWithdraw {}, + #[error("Nothing to withdraw")] + NothingToWithdraw, #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { @@ -56,19 +57,21 @@ pub enum ContractError { }, #[error("There are unclaimed rewards available. Claim them before attempting to bond/unbond")] - UnclaimedRewards {}, + UnclaimedRewards, #[error("Trying to bond/unbond at a late time before the new/latest epoch has been created")] - NewEpochNotCreatedYet {}, + NewEpochNotCreatedYet, #[error("Nothing to claim")] - NothingToClaim {}, + NothingToClaim, - #[error("Nothing to claim")] - InvalidReward {}, + #[error("Something is off with the reward calculation, user share is above 1. Can't claim.")] + InvalidShare, - #[error("No Swap Route found for assets {asset1} and {asset2}")] - NoSwapRoute { asset1: String, asset2: String }, + #[error( + "Invalid reward amount. Reward: {reward}, but only {available} available in the epoch." + )] + InvalidReward { reward: Uint128, available: Uint128 }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 390016e5..9043699a 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -3,23 +3,24 @@ use cosmwasm_std::{ StdResult, SubMsg, Timestamp, Uint64, WasmMsg, }; use cw_utils::PaymentError; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Config, EpochResponse}; -use white_whale_std::constants::LP_SYMBOL; -use white_whale_std::epoch_manager::epoch_manager::EpochConfig; +use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Config}; +use white_whale_std::constants::{DAY_IN_SECONDS, LP_SYMBOL}; +use white_whale_std::epoch_manager::epoch_manager::{EpochConfig, EpochResponse}; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, }; use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; -use crate::queries::{get_claimable_epochs, get_current_epoch}; +use crate::queries::query_claimable; use crate::state::CONFIG; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { - if growth_rate > Decimal::percent(100) { - return Err(ContractError::InvalidGrowthRate {}); - } + ensure!( + growth_rate <= Decimal::percent(100), + ContractError::InvalidGrowthRate + ); Ok(()) } @@ -52,41 +53,46 @@ pub fn validate_funds(deps: &DepsMut, info: &MessageInfo) -> Result Result<(), ContractError> { +pub fn validate_claimed(deps: &DepsMut, info: &MessageInfo) -> Result<(), ContractError> { // Do a smart query for Claimable - let claimable_rewards: ClaimableEpochsResponse = get_claimable_epochs(deps.as_ref()).unwrap(); - // If epochs is greater than none - if !claimable_rewards.epochs.is_empty() { - return Err(ContractError::UnclaimedRewards {}); - } + let claimable_rewards: ClaimableEpochsResponse = + query_claimable(deps.as_ref(), Some(info.sender.to_string())).unwrap(); + // ensure the user has nothing to claim + ensure!( + claimable_rewards.epochs.is_empty(), + ContractError::UnclaimedRewards + ); Ok(()) } /// 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. -/// global_index timestamp issues when querying the weight. pub fn validate_bonding_for_current_epoch(deps: &DepsMut, env: &Env) -> Result<(), ContractError> { - let epoch_response: EpochResponse = get_current_epoch(deps.as_ref()).unwrap(); + 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 current_epoch = epoch_response.epoch; - let current_time = env.block.time.seconds(); - const DAY_IN_SECONDS: u64 = 86_400u64; - // Check if the current time is more than a day after the epoch start time - // to avoid potential overflow - if current_epoch.id != Uint64::zero() { + if current_epoch.id != 0u64 { + let current_time = env.block.time.seconds(); + let start_time_seconds = current_epoch .start_time .seconds() .checked_add(DAY_IN_SECONDS); + match start_time_seconds { Some(start_time_plus_day) => { - if current_time > start_time_plus_day { - return Err(ContractError::NewEpochNotCreatedYet {}); - } + ensure!( + current_time <= start_time_plus_day, + ContractError::NewEpochNotCreatedYet + ); } - None => return Err(ContractError::Unauthorized {}), + None => return Err(ContractError::Unauthorized), } } @@ -107,6 +113,7 @@ pub fn calculate_epoch( let elapsed_time = Uint64::new(timestamp.nanos()).checked_sub(genesis_epoch_config.genesis_epoch)?; + let epoch = elapsed_time .checked_div(epoch_duration)? .checked_add(Uint64::one())?; @@ -117,20 +124,23 @@ pub fn calculate_epoch( // Used in FillRewards to search the funds for LP tokens and withdraw them // If we do get some LP tokens to withdraw they could be swapped to whale in the reply pub fn handle_lp_tokens( - info: &MessageInfo, - config: &white_whale_std::bonding_manager::Config, + funds: &Vec, + config: &Config, submessages: &mut Vec, ) -> Result<(), ContractError> { - let lp_tokens: Vec<&Coin> = info - .funds + 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 { - // LP tokens have the format "{pair_label}.pool.{identifier}.{LP_SYMBOL}", get the identifier and not the LP SYMBOL - let pool_identifier = lp_token.denom.split(".pool.").collect::>()[1] - .split('.') - .collect::>()[0]; + let pool_identifier = + extract_pool_identifier(&lp_token.denom).ok_or(ContractError::AssetMismatch)?; + + println!("pool_identifier: {:?}", pool_identifier); // if LP Tokens ,verify and withdraw then swap to whale let lp_withdrawal_msg = white_whale_std::pool_manager::ExecuteMsg::WithdrawLiquidity { @@ -153,12 +163,31 @@ pub fn handle_lp_tokens( Ok(()) } -// Used in FillRewards to search the funds for coins that are neither LP tokens nor whale and swap them to whale +/// Extracts the pool identifier from an LP token denom. +/// LP tokens have the format "{pair_label}.pool.{identifier}.{LP_SYMBOL}", get the +/// identifier and not the LP SYMBOL. The identifier can contain dots, slashes, etc. +fn extract_pool_identifier(lp_token_denom: &str) -> Option<&str> { + // Split the string at ".pool." to isolate the part after ".pool." + let parts: Vec<&str> = lp_token_denom.splitn(2, ".pool.").collect(); + if parts.len() < 2 { + return None; + } + + // Split by the last dot to isolate the identifier from "{LP_SYMBOL}" + let after_pool = parts[1]; + let last_dot_pos = after_pool.rfind('.').unwrap_or(after_pool.len()); + + // Take everything before the last dot to get the identifier + Some(&after_pool[..last_dot_pos]) +} + +// Used in FillRewards to search the funds for coins that are neither LP tokens nor the distribution_denom +// and swap them to distribution_denom pub fn swap_coins_to_main_token( coins: Vec, deps: &DepsMut, config: Config, - whale: &mut Coin, + to_be_distribution_asset: &mut Coin, distribution_denom: &String, messages: &mut Vec, ) -> Result<(), ContractError> { @@ -198,7 +227,8 @@ pub fn swap_coins_to_main_token( } // check if the pool has any assets, if not skip the swap - // Note we are only checking the first operation here. Might be better to another loop to check all operations + // Note we are only checking the first operation here. + // Might be better to another loop to check all operations let pool_query = white_whale_std::pool_manager::QueryMsg::Pool { pool_identifier: swap_routes .swap_route @@ -219,32 +249,36 @@ pub fn swap_coins_to_main_token( } }); - let simulate: SimulateSwapOperationsResponse = deps.querier.query_wasm_smart( - config.pool_manager_addr.to_string(), - &white_whale_std::pool_manager::QueryMsg::SimulateSwapOperations { - offer_amount: coin.amount, - operations: swap_routes.swap_route.swap_operations.clone(), - }, - )?; - // Add the simulate amount received to the whale amount, if the swap fails this should also be rolled back - whale.amount = whale.amount.checked_add(simulate.amount)?; + let simulate_swap_operations_response: SimulateSwapOperationsResponse = + deps.querier.query_wasm_smart( + config.pool_manager_addr.to_string(), + &white_whale_std::pool_manager::QueryMsg::SimulateSwapOperations { + offer_amount: coin.amount, + operations: swap_routes.swap_route.swap_operations.clone(), + }, + )?; + // Add the simulate amount received to the distribution_denom amount, if the swap fails this should + // also be rolled back + to_be_distribution_asset.amount = to_be_distribution_asset + .amount + .checked_add(simulate_swap_operations_response.amount)?; if !skip_swap { // Prepare a swap message, use the simulate amount as the minimum receive // and 1% slippage to ensure we get at least what was simulated to be received - let msg = white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { - operations: swap_routes.swap_route.swap_operations.clone(), - minimum_receive: Some(simulate.amount), - receiver: None, - max_spread: Some(Decimal::percent(5)), - }; - let binary_msg = to_json_binary(&msg)?; - let wrapped_msg = WasmMsg::Execute { + let swap_msg = WasmMsg::Execute { contract_addr: config.pool_manager_addr.to_string(), - msg: binary_msg, + msg: to_json_binary( + &white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { + operations: swap_routes.swap_route.swap_operations.clone(), + minimum_receive: Some(simulate_swap_operations_response.amount), + receiver: None, + max_spread: Some(Decimal::percent(5)), + }, + )?, funds: vec![coin.clone()], }; - messages.push(wrapped_msg.into()); + messages.push(swap_msg.into()); } } Ok(()) diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 82cd8726..61e59647 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -2,8 +2,8 @@ use std::collections::{HashSet, VecDeque}; use white_whale_std::epoch_manager::epoch_manager::ConfigResponse; use cosmwasm_std::{ - to_json_binary, Addr, Decimal, Deps, Order, QueryRequest, StdError, StdResult, Timestamp, - Uint128, Uint64, WasmQuery, + to_json_binary, Decimal, Deps, Order, QueryRequest, StdError, StdResult, Timestamp, Uint128, + Uint64, WasmQuery, }; use cw_storage_plus::Bound; @@ -11,7 +11,7 @@ use white_whale_std::bonding_manager::{ Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, UnbondingResponse, WithdrawableResponse, }; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch, EpochResponse}; +use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; use white_whale_std::epoch_manager::epoch_manager::QueryMsg; use crate::helpers; @@ -24,53 +24,62 @@ pub(crate) fn query_config(deps: Deps) -> StdResult { CONFIG.load(deps.storage) } -/// Queries the current bonded amount of the given address. -pub(crate) fn query_bonded(deps: Deps, address: String) -> StdResult { - let address = deps.api.addr_validate(&address)?; - - let bonds: Vec = BOND - .prefix(&address) - .range(deps.storage, None, None, Order::Ascending) - .take(BONDING_ASSETS_LIMIT) - .map(|item| { - let (_, bond) = item?; - Ok(bond) - }) - .collect::>>()?; +/// Queries the current bonded amount of the given address. If no address is provided, returns +/// the global bonded amount. +pub(crate) fn query_bonded(deps: Deps, address: Option) -> StdResult { + let (total_bonded, bonded_assets, first_bonded_epoch_id) = if let Some(address) = address { + let address = deps.api.addr_validate(&address)?; + + let bonds: Vec = BOND + .prefix(&address) + .range(deps.storage, None, None, Order::Ascending) + .take(BONDING_ASSETS_LIMIT) + .map(|item| { + let (_, bond) = item?; + Ok(bond) + }) + .collect::>>()?; + + // if it doesn't have bonded, return empty response + if bonds.is_empty() { + return Ok(BondedResponse { + total_bonded: Uint128::zero(), + bonded_assets: vec![], + first_bonded_epoch_id: Some(Uint64::zero()), + }); + } - // if it doesn't have bonded, return empty response - if bonds.is_empty() { - return Ok(BondedResponse { - total_bonded: Uint128::zero(), - bonded_assets: vec![], - first_bonded_epoch_id: Uint64::zero(), - }); - } + let mut total_bonded = Uint128::zero(); + let mut bonded_assets = vec![]; - let mut total_bonded = Uint128::zero(); - let mut bonded_assets = vec![]; + // 1 January 2500 + let mut first_bond_timestamp = Timestamp::from_seconds(16725229261u64); - let mut first_bond_timestamp = Timestamp::from_seconds(16725229261u64); + for bond in bonds { + if bond.timestamp.seconds() < first_bond_timestamp.seconds() { + first_bond_timestamp = bond.timestamp; + } - for bond in bonds { - if bond.timestamp.seconds() < first_bond_timestamp.seconds() { - first_bond_timestamp = bond.timestamp; + total_bonded = total_bonded.checked_add(bond.asset.amount)?; + bonded_assets.push(bond.asset); } - total_bonded = total_bonded.checked_add(bond.asset.amount)?; - bonded_assets.push(bond.asset); - } - // TODO: This is hardcoded, either we add to config the address of epoch manager and query - // or we store the genesis epoch itself in the bonding manager - // Query epoch manager for EpochConfig - let epoch_config: ConfigResponse = - deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "contract0".to_string(), - msg: to_json_binary(&QueryMsg::Config {})?, - }))?; - - let first_bonded_epoch_id = - helpers::calculate_epoch(epoch_config.epoch_config, first_bond_timestamp)?; + let config = CONFIG.load(deps.storage)?; + // Query epoch manager for EpochConfig + let epoch_config: ConfigResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: config.epoch_manager_addr.to_string(), + msg: to_json_binary(&QueryMsg::Config {})?, + }))?; + + let first_bonded_epoch_id = + helpers::calculate_epoch(epoch_config.epoch_config, first_bond_timestamp)?; + + (total_bonded, bonded_assets, Some(first_bonded_epoch_id)) + } else { + let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); + (global_index.bonded_amount, global_index.bonded_assets, None) + }; Ok(BondedResponse { total_bonded, @@ -188,6 +197,7 @@ pub(crate) fn query_weight( total_bond_weight = total_bond_weight.checked_add(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 { @@ -197,7 +207,6 @@ pub(crate) fn query_weight( .ok_or_else(|| StdError::generic_err("Global index not found"))? }; - // If a global weight from an Epoch was passed, use that to get the weight, otherwise use the current global index weight global_index.weight = get_weight( timestamp, global_index.weight, @@ -208,7 +217,11 @@ pub(crate) fn query_weight( // 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 = Decimal::from_ratio(total_bond_weight, global_index.weight); + let share = if global_index.weight.is_zero() { + Decimal::zero() + } else { + Decimal::from_ratio(total_bond_weight, global_index.weight) + }; Ok(BondingWeightResponse { address: address.to_string(), @@ -219,60 +232,31 @@ pub(crate) fn query_weight( }) } -/// Queries the total amount of assets that have been bonded to the contract. -pub fn query_total_bonded(deps: Deps) -> StdResult { - let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - Ok(BondedResponse { - total_bonded: global_index.bonded_amount, - bonded_assets: global_index.bonded_assets, - first_bonded_epoch_id: Default::default(), //ignore this parameter here - }) -} - /// Queries the global index pub fn query_global_index(deps: Deps) -> StdResult { let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); Ok(global_index) } -/// Returns the current epoch, which is the last on the EPOCHS map. -pub fn get_current_epoch(deps: Deps) -> StdResult { - let option = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .next(); - - let epoch = match option { - Some(Ok((_, epoch))) => epoch, - _ => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. pub fn get_expiring_epoch(deps: Deps) -> StdResult> { let config = CONFIG.load(deps.storage)?; // Adding 1 because we store the future epoch in the map also, so grace_period + 1 - let grace_period = config.grace_period.u64() + 1; + let grace_period_plus_future_epoch = config.grace_period.u64() + 1u64; // Take grace_period + 1 and then slice last one off - let mut epochs = EPOCHS + let epochs = EPOCHS .range(deps.storage, None, None, Order::Descending) - .take(grace_period as usize) + .take(grace_period_plus_future_epoch as usize) .map(|item| { let (_, epoch) = item?; Ok(epoch) }) - .collect::>>()?; - - if epochs.len() > 1 { - // First the future epoch from stack - epochs.pop_front(); - } + .collect::>>()?; // if the epochs vector's length is the same as the grace period it means there is one epoch that // is expiring once the new one is created i.e. the last epoch in the vector - if epochs.len() == config.grace_period.u64() as usize { + if epochs.len() == grace_period_plus_future_epoch as usize { let expiring_epoch: Epoch = epochs.into_iter().last().unwrap_or_default(); Ok(Some(expiring_epoch)) } else { @@ -298,44 +282,63 @@ pub fn get_claimable_epochs(deps: Deps) -> StdResult { }) .collect::>>()?; - if epochs.len() > 1 { - // First the future epoch from stack - epochs.pop_front(); - } + println!("epochs: {:?}", epochs); + + // Remove the upcoming epoch from stack + epochs.pop_front(); epochs.retain(|epoch| !epoch.available.is_empty()); + println!("epochs: {:?}", epochs); + Ok(ClaimableEpochsResponse { epochs: epochs.into(), }) } -/// Returns the epochs that can be claimed by the given address. -pub fn query_claimable(deps: Deps, address: &Addr) -> StdResult { +/// Returns the epochs that can be claimed by the given address. If no address is provided, +/// returns all possible epochs stored in the contract that can potentially be claimed. +pub fn query_claimable(deps: Deps, address: Option) -> StdResult { let mut claimable_epochs = get_claimable_epochs(deps)?.epochs; - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, address)?; - // filter out epochs that have already been claimed by the user - if let Some(last_claimed_epoch) = last_claimed_epoch { - claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); - } else { - // if the user doesn't have any last_claimed_epoch two things might be happening: - // 1- the user has never bonded before - // 2- the user has bonded, but never claimed any rewards so far + // 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 bonded_response: BondedResponse = query_bonded(deps, address.to_string())?; + let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &address)?; - if bonded_response.bonded_assets.is_empty() { - // the user has never bonded before, therefore it shouldn't be able to claim anything - claimable_epochs.clear(); + // filter out epochs that have already been claimed by the user + if let Some(last_claimed_epoch) = last_claimed_epoch { + claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); } else { - // the user has bonded, but never claimed any rewards so far - claimable_epochs.retain(|epoch| epoch.id > bonded_response.first_bonded_epoch_id); - } - }; - // filter out epochs that have no available fees. This would only happen in case the grace period - // gets increased after epochs have expired, which would lead to make them available for claiming - // again without any available rewards, as those were forwarded to newer epochs. - claimable_epochs.retain(|epoch| !epoch.available.is_empty()); + // if the user doesn't have any last_claimed_epoch two things might be happening: + // 1- the user has never bonded before + // 2- the user has bonded, but never claimed any rewards so far + + let bonded_response: BondedResponse = query_bonded(deps, Some(address.into_string()))?; + println!("bonded_responsebonded_response: {:?}", bonded_response); + if bonded_response.bonded_assets.is_empty() { + // the user has never bonded before, therefore it shouldn't be able to claim anything + claimable_epochs.clear(); + } else { + // the user has bonded, but never claimed any rewards so far. The first_bonded_epoch_id + // value should always be Some here, as `query_bonded` will always return Some. + match bonded_response.first_bonded_epoch_id { + Some(first_bonded_epoch_id) => { + // keep all epochs that are newer than the first bonded epoch + claimable_epochs.retain(|epoch| epoch.id > first_bonded_epoch_id); + } + None => { + // for sanity, it should never happen + claimable_epochs.clear(); + } + } + } + }; + // filter out epochs that have no available fees. This would only happen in case the grace period + // gets increased after epochs have expired, which would lead to make them available for claiming + // again without any available rewards, as those were forwarded to newer epochs. + claimable_epochs.retain(|epoch| !epoch.available.is_empty()); + } Ok(ClaimableEpochsResponse { epochs: claimable_epochs, diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 7331461c..6210e2fc 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,12 +1,9 @@ -use crate::queries::query_bonded; use crate::ContractError; use cosmwasm_std::{ Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Timestamp, Uint128, Uint64, }; use cw_storage_plus::{Item, Map}; -use white_whale_std::bonding_manager::{ - Bond, BondedResponse, ClaimableEpochsResponse, Config, Epoch, EpochResponse, GlobalIndex, -}; +use white_whale_std::bonding_manager::{Bond, Config, Epoch, GlobalIndex}; type Denom = str; @@ -15,10 +12,6 @@ pub const CONFIG: Item = Item::new("config"); pub const BOND: Map<(&Addr, &Denom), Bond> = Map::new("bond"); pub const UNBOND: Map<(&Addr, &Denom, u64), Bond> = Map::new("unbond"); pub const GLOBAL: Item = Item::new("global"); -pub type EpochID = [u8]; - -pub const REWARDS_BUCKET: Map<&EpochID, &Epoch> = Map::new("rewards_bucket"); - pub const LAST_CLAIMED_EPOCH: Map<&Addr, Uint64> = Map::new("last_claimed_epoch"); pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); @@ -43,6 +36,7 @@ pub fn update_local_weight( let denom: &String = &bond.asset.denom; + //todo remove? done outside of this function. Or remove outside BOND.save(deps.storage, (&address, denom), &bond)?; Ok(bond) @@ -66,6 +60,7 @@ pub fn update_global_weight( global_index.timestamp = timestamp; + //todo remove? done outside of this function. Or remove outside GLOBAL.save(deps.storage, &global_index)?; Ok(global_index) @@ -93,32 +88,6 @@ pub fn get_weight( Ok(weight.checked_add(amount.checked_mul(time_factor)? * growth_rate)?) } -/// Returns the current epoch, which is the last on the EPOCHS map. -pub fn get_current_epoch(deps: Deps) -> StdResult { - let option = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .next(); - - let epoch = match option { - Some(Ok((_, epoch))) => epoch, - _ => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - -/// Returns the [Epoch] with the given id. -pub fn get_epoch(deps: Deps, id: Uint64) -> StdResult { - let option = EPOCHS.may_load(deps.storage, &id.to_be_bytes())?; - - let epoch = match option { - Some(epoch) => epoch, - None => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. pub fn get_expiring_epoch(deps: Deps) -> StdResult> { @@ -143,54 +112,3 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { Ok(None) } } - -/// Returns the epochs that are within the grace period, i.e. the ones which fees can still be claimed. -/// The result is ordered by epoch id, descending. Thus, the first element is the current epoch. -pub fn get_claimable_epochs(deps: Deps) -> StdResult { - let grace_period = CONFIG.load(deps.storage)?.grace_period; - - let epochs = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .take(grace_period.u64() as usize) - .map(|item| { - let (_, epoch) = item?; - Ok(epoch) - }) - .collect::>>()?; - - Ok(ClaimableEpochsResponse { epochs }) -} - -/// Returns the epochs that can be claimed by the given address. -pub fn query_claimable(deps: Deps, address: &Addr) -> StdResult { - let mut claimable_epochs = get_claimable_epochs(deps)?.epochs; - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, address)?; - - // filter out epochs that have already been claimed by the user - if let Some(last_claimed_epoch) = last_claimed_epoch { - claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); - } else { - // if the user doesn't have any last_claimed_epoch two things might be happening: - // 1- the user has never bonded before - // 2- the user has bonded, but never claimed any rewards so far - - let bonded_response: BondedResponse = query_bonded(deps, address.to_string())?; - - if bonded_response.bonded_assets.is_empty() { - // the user has never bonded before, therefore it shouldn't be able to claim anything - claimable_epochs.clear(); - } else { - // the user has bonded, but never claimed any rewards so far - claimable_epochs.retain(|epoch| epoch.id > bonded_response.first_bonded_epoch_id); - } - }; - - // filter out epochs that have no available fees. This would only happen in case the grace period - // gets increased after epochs have expired, which would lead to make them available for claiming - // again without any available rewards, as those were forwarded to newer epochs. - claimable_epochs.retain(|epoch| !epoch.available.is_empty()); - - Ok(ClaimableEpochsResponse { - epochs: claimable_epochs, - }) -} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs index e4195559..a6eb7700 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -8,7 +8,8 @@ use crate::tests::robot::TestingRobot; use crate::tests::test_helpers; use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; -use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse}; +use crate::ContractError; +use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse, Epoch, GlobalIndex}; use super::test_helpers::get_epochs; @@ -16,26 +17,156 @@ use super::test_helpers::get_epochs; fn test_claimable_epochs() { let mut robot = TestingRobot::default(); let grace_period = Uint64::new(21); + let creator = robot.sender.clone(); - let epochs = test_helpers::get_epochs(); - let binding = epochs.clone(); - let mut claimable_epochs = binding - .iter() - .rev() - .take(grace_period.u64() as usize) - .collect::>(); - claimable_epochs.pop_front(); + 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![], + }; robot .instantiate_default() - .add_epochs_to_state(epochs) - .query_claimable_epochs(None, |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), claimable_epochs.len()); - for (e, a) in epochs.iter().zip(claimable_epochs.iter()) { - assert_eq!(e, *a); - } - }); + .fast_forward(259_200) + .create_epoch(|result| { + result.unwrap(); + }) + .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(); + }, + ) + .swap( + creator.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .create_epoch(|result| { + result.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(); + }, + ); + + let expected_epochs = vec![Epoch { + id: Uint64::new(2u64), + start_time: Timestamp::from_nanos(1571970229879305533), + total: vec![coin(1009u128, "uwhale")], + available: vec![coin(1009u128, "uwhale")], + claimed: vec![], + global_index: GlobalIndex { + bonded_amount: Default::default(), + bonded_assets: vec![], + timestamp: Timestamp::from_nanos(1572056619879305533), + weight: Default::default(), + }, + }]; + + robot.query_claimable_epochs(None, |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), expected_epochs.len()); + + for (index, epoch) in epochs.iter().enumerate() { + assert_eq!(expected_epochs[index], epoch.clone()); + } + }); } #[test] @@ -60,7 +191,7 @@ fn test_claim_successfully() { }, extra_fees: vec![], }; - get_epochs(); + robot .instantiate_default() .bond( @@ -83,18 +214,20 @@ fn test_claim_successfully() { first_bonded_epoch_id: Default::default(), }, ) - .fast_forward(10u64) + .fast_forward(100_000u64) + .create_epoch(|result| { + result.unwrap(); + }) .assert_bonding_weight_response( sender.to_string(), BondingWeightResponse { address: sender.to_string(), - weight: Uint128::new(11_000u128), - global_weight: Uint128::new(11_000u128), + weight: Uint128::new(100_001_000u128), + global_weight: Uint128::new(100_001_000u128), share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797429879305533u64), + timestamp: Timestamp::from_nanos(1571897419879305533u64), }, ) - .fast_forward(10u64) .bond( sender.clone(), Coin { @@ -121,47 +254,16 @@ fn test_claim_successfully() { first_bonded_epoch_id: Default::default(), }, ) - .fast_forward(10u64) - .bond( - another_sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(5_000u128), - }, - &coins(5_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(104_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(104_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), - }, - ) - .assert_bonding_weight_response( - another_sender.to_string(), - BondingWeightResponse { - address: another_sender.to_string(), - weight: Uint128::new(55_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(55_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), - }, - ) .query_total_bonded(|res| { let bonded_response = res.unwrap().1; assert_eq!( bonded_response, BondedResponse { - total_bonded: Uint128::new(9_000u128), + total_bonded: Uint128::new(4_000u128), bonded_assets: vec![ Coin { denom: "ampWHALE".to_string(), - amount: Uint128::new(6_000u128), + amount: Uint128::new(1_000u128), }, Coin { denom: "bWHALE".to_string(), @@ -178,86 +280,239 @@ fn test_claim_successfully() { assert_eq!(epochs.len(), 0); }); - robot.create_pair( - sender.clone(), - asset_infos.clone(), - pool_fees.clone(), - white_whale_std::pool_manager::PoolType::ConstantProduct, - Some("whale-uusdc".to_string()), - vec![coin(1000, "uwhale")], - |result| { - result.unwrap(); - }, - ); - - // Lets try to add liquidity - robot.provide_liquidity( - sender.clone(), - "whale-uusdc".to_string(), - vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1000000000u128), + println!("-------"); + robot + .create_pair( + sender.clone(), + asset_infos.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); }, - Coin { + ) + .provide_liquidity( + sender.clone(), + "whale-uusdc".to_string(), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + |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(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .swap( + sender.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); }, - ], - |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(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) - .to_string() - }) - })); - }, - ); - - robot.swap( - sender.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| { + ) + .swap( + sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .fast_forward(90_000) + .create_epoch(|result| { result.unwrap(); - }, - ); + }) + .swap( + sender.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(); + }, + ) + .swap( + sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ); - robot - .create_new_epoch() - .query_claimable_epochs_live(Some(sender.clone()), |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 1); - }); + robot.query_claimable_epochs_live(None, |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); + }); + robot.query_claimable_epochs_live(Some(sender.clone()), |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); + }); - robot.claim(sender, |res| { + robot.claim(sender.clone(), |res| { let result = res.unwrap(); assert!(result.events.iter().any(|event| { event .attributes .iter() - .any(|attr| attr.key == "amount" && attr.value == "448uwhale") + .any(|attr| attr.key == "amount" && attr.value == "571uwhale") })); }); - robot.claim(another_sender, |res| { - let result = res.unwrap(); - assert!(result.events.iter().any(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "amount" && attr.value == "560uwhale") - })); + robot.claim(another_sender.clone(), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NothingToClaim => {} + _ => { + panic!("Wrong error type, should return ContractError::NothingToClaim") + } + } + }); + + robot + .bond( + another_sender.clone(), + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(9_000u128), + }, + &coins(9_000u128, "ampWHALE"), + |res| { + res.unwrap(); + }, + ) + .assert_bonded_response( + another_sender.to_string(), + BondedResponse { + total_bonded: Uint128::new(9_000u128), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(9_000u128), + }], + first_bonded_epoch_id: Uint64::new(3u64), + }, + ); + + robot + .fast_forward(100_000) + .create_epoch(|result| { + result.unwrap(); + println!("*****"); + }) + .swap( + another_sender.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(); + }, + ) + .swap( + another_sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + sender.clone(), + coin(5_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(5_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + sender.clone(), + coin(5_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(5_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + robot.query_claimable_epochs_live(Some(another_sender.clone()), |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); }); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index 91166618..0ad6aa84 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -1,6 +1,8 @@ use cosmwasm_std::{Addr, Decimal, Uint64}; +use crate::state::BONDING_ASSETS_LIMIT; use crate::tests::robot::TestingRobot; +use crate::ContractError; use white_whale_std::bonding_manager::Config; #[test] @@ -15,7 +17,7 @@ fn test_instantiate_successfully() { &vec![], ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000u64), @@ -40,12 +42,10 @@ fn test_instantiate_unsuccessfully() { ], &vec![], |error| { - println!("1 --{error:?}"); - println!("2 --{:?}", error.root_cause()); - //println!("3 --{:?}", error.root_cause().downcast_ref::()); - // assert_eq!( - // error.root_cause().downcast_ref::().unwrap(), - // &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3)); + assert_eq!( + error.root_cause().downcast_ref::().unwrap(), + &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3) + ); }, ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs index b05b9f91..143289a2 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs @@ -12,9 +12,8 @@ use crate::tests::test_helpers; fn test_fill_rewards_from_pool_manager() { let mut robot = TestingRobot::default(); let creator = robot.sender.clone(); - let epochs = test_helpers::get_epochs(); - let asset_infos = vec!["uwhale".to_string(), "uusdc".to_string()]; + let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; // Default Pool fees white_whale_std::pool_network::pair::PoolFee // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% @@ -34,10 +33,13 @@ fn test_fill_rewards_from_pool_manager() { robot .instantiate_default() - .add_epochs_to_state(epochs) + .fast_forward(90_000) + .create_epoch(|result| { + result.unwrap(); + }) .create_pair( creator.clone(), - asset_infos.clone(), + asset_denoms.clone(), pool_fees.clone(), white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc".to_string()), @@ -54,29 +56,26 @@ fn test_fill_rewards_from_pool_manager() { vec![ Coin { denom: "uwhale".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000_000_000u128), }, Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000_000_000u128), }, ], |result| { - println!("{:?}", result.as_ref().unwrap()); // 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(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) + == (Uint128::from(1_000_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) .to_string() }) })); }, ); - println!("{:?}", robot.app.wrap().query_all_balances(creator.clone())); - // Lets try to add a swap route let swap_route_1 = SwapRoute { offer_asset_denom: "uusdc".to_string(), @@ -88,7 +87,7 @@ fn test_fill_rewards_from_pool_manager() { }], }; robot.add_swap_routes(creator.clone(), vec![swap_route_1], |res| { - println!("{:?}", res.unwrap()); + res.unwrap(); }); robot.swap( @@ -121,7 +120,7 @@ fn test_fill_rewards_from_pool_manager() { robot.create_pair( creator.clone(), - asset_infos.clone(), + asset_denoms.clone(), pool_fees.clone(), white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc-second".to_string()), @@ -143,7 +142,7 @@ fn test_fill_rewards_from_pool_manager() { // create another pair to collect another fee robot.create_pair( creator.clone(), - asset_infos, + asset_denoms, pool_fees, white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc-third".to_string()), @@ -168,7 +167,47 @@ fn test_fill_rewards_from_pool_manager() { "factory/contract2/uwhale-uusdc.pool.whale-uusdc.uLP", )], |res| { - println!("{:?}", res.unwrap()); + res.unwrap(); + }, + ); + + let bonding_manager_addr = robot.bonding_manager_addr.clone(); + let bonding_manager_balances = robot + .app + .wrap() + .query_all_balances(bonding_manager_addr.clone()) + .unwrap(); + assert_eq!(bonding_manager_balances.len(), 1); + assert_eq!(bonding_manager_balances[0].amount, Uint128::from(4998u128)); + + // send some random asset that doesn't have swap routes + robot.fill_rewards_lp( + creator.clone(), + vec![coin(1000, "non_whitelisted_asset")], + |res| { + res.unwrap(); }, ); + + let bonding_manager_addr = robot.bonding_manager_addr.clone(); + let bonding_manager_balances = robot + .app + .wrap() + .query_all_balances(bonding_manager_addr.clone()) + .unwrap(); + assert_eq!(bonding_manager_balances.len(), 2); + assert_eq!( + bonding_manager_balances, + vec![ + // wasn't swapped + Coin { + denom: "non_whitelisted_asset".to_string(), + amount: Uint128::from(1000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(4998u128), + }, + ] + ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs index d2e422a8..7f848598 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs @@ -1,7 +1,7 @@ use anyhow::Error; use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{ - coin, from_json, Addr, Coin, Decimal, Empty, OwnedDeps, StdResult, Uint128, Uint64, + coin, from_json, Addr, Binary, Coin, Decimal, Empty, OwnedDeps, StdResult, Uint128, Uint64, }; // use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; use cw_multi_test::{ @@ -84,7 +84,7 @@ pub struct TestingRobot { /// instantiate / execute messages impl TestingRobot { pub(crate) fn default() -> Self { - let sender = Addr::unchecked("owner"); + let sender = Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); let another_sender = Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"); let sender_3 = Addr::unchecked("migaloo1ludaslnu24p5eftw499f7ngsc2jkzqdsrvxt75"); @@ -196,16 +196,6 @@ impl TestingRobot { .unwrap(); println!("hook_registration_msg: {:?}", resp); - // self.fast_forward(10); - let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; - self.app - .execute_contract( - self.sender.clone(), - epoch_manager_addr.clone(), - &new_epoch_msg, - &[], - ) - .unwrap(); let msg = white_whale_std::pool_manager::InstantiateMsg { bonding_manager_addr: bonding_manager_addr.clone().to_string(), @@ -240,6 +230,7 @@ impl TestingRobot { self.app .execute_contract(self.sender.clone(), bonding_manager_addr.clone(), &msg, &[]) .unwrap(); + self.bonding_manager_addr = bonding_manager_addr; self.pool_manager_addr = pool_manager_addr; self.epoch_manager_addr = epoch_manager_addr; @@ -387,20 +378,6 @@ impl TestingRobot { ) .unwrap(); } - CONFIG - .save( - &mut self.owned_deps.storage, - &Config { - distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), - growth_rate: Decimal::one(), - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], - grace_period: Uint64::new(21), - owner: Addr::unchecked("owner"), - pool_manager_addr: Addr::unchecked("pool_manager"), - }, - ) - .unwrap(); self } @@ -427,7 +404,7 @@ fn instantiate_contract( robot.sender.clone(), &msg, funds, - "White Whale Lair".to_string(), + "Bonding Manager".to_string(), Some(robot.sender.clone().to_string()), ) } @@ -478,27 +455,24 @@ impl TestingRobot { address: Option, response: impl Fn(StdResult<(&mut Self, Vec)>), ) -> &mut Self { - let query_res = if let Some(address) = address { - query( - self.owned_deps.as_ref(), - self.env.clone(), - QueryMsg::Claimable { - addr: address.to_string(), - }, - ) - .unwrap() + let query_res: ClaimableEpochsResponse = if let Some(address) = address { + self.app + .wrap() + .query_wasm_smart( + &self.bonding_manager_addr, + &QueryMsg::Claimable { + addr: address.to_string(), + }, + ) + .unwrap() } else { - query( - self.owned_deps.as_ref(), - self.env.clone(), - QueryMsg::ClaimableEpochs {}, - ) - .unwrap() + self.app + .wrap() + .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::ClaimableEpochs {}) + .unwrap() }; - let res: ClaimableEpochsResponse = from_json(query_res).unwrap(); - - response(Ok((self, res.epochs))); + response(Ok((self, query_res.epochs))); self } @@ -731,6 +705,23 @@ impl TestingRobot { self } + + #[track_caller] + pub(crate) fn create_epoch( + &mut self, + result: impl Fn(Result), + ) -> &mut Self { + let sender = self.another_sender.clone(); + + result(self.app.execute_contract( + sender, + self.epoch_manager_addr.clone(), + &white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch, + &[], + )); + + self + } } /// assertions diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index 96f586cc..a655db2e 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -12,7 +12,7 @@ fn test_update_config_successfully() { robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -66,7 +66,7 @@ fn test_update_config_unsuccessfully() { robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -92,7 +92,7 @@ fn test_update_config_unsuccessfully() { }, ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -118,7 +118,7 @@ fn test_update_config_unsuccessfully() { }, ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index ef5f4b09..61fcb26f 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Api, DepsMut, Env, MessageInfo, Response, SubMsg}; +use cosmwasm_std::{ensure, Api, DepsMut, Env, MessageInfo, Response, SubMsg}; use white_whale_std::epoch_manager::epoch_manager::EpochConfig; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; @@ -35,11 +35,16 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= current_epoch.start_time, + ContractError::GenesisEpochHasNotStarted ); if env @@ -51,6 +56,7 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= env.block.time, + ContractError::InvalidStartTime + ); - if msg.epoch_config.genesis_epoch.u64() != msg.start_epoch.start_time.nanos() { - return Err(ContractError::EpochConfigMismatch); - } + ensure!( + msg.epoch_config.genesis_epoch.u64() == msg.start_epoch.start_time.nanos(), + ContractError::EpochConfigMismatch + ); ADMIN.set(deps.branch(), Some(info.sender))?; EPOCHS.save(deps.storage, msg.start_epoch.id, &msg.start_epoch)?; diff --git a/contracts/liquidity_hub/epoch-manager/src/error.rs b/contracts/liquidity_hub/epoch-manager/src/error.rs index bb6e61aa..25c7d97b 100644 --- a/contracts/liquidity_hub/epoch-manager/src/error.rs +++ b/contracts/liquidity_hub/epoch-manager/src/error.rs @@ -33,6 +33,9 @@ pub enum ContractError { #[error("The current epoch epoch has not expired yet.")] CurrentEpochNotExpired, + #[error("The genesis epoch has not started yet.")] + GenesisEpochHasNotStarted, + #[error("start_time must be in the future.")] InvalidStartTime, diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index 656b6c84..b9e9da96 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -112,9 +112,6 @@ pub enum ContractError { #[error("An overflow occurred when attempting to construct a decimal")] DecimalOverflow, - #[error("The token factory feature is not enabled")] - TokenFactoryNotEnabled, - #[error("{0}")] OverflowError(#[from] OverflowError), diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 9c19b832..0c679f11 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -174,15 +174,6 @@ pub fn create_pool( attributes.push(attr("lp_asset", lp_asset)); - #[cfg(all( - not(feature = "token_factory"), - not(feature = "osmosis_token_factory"), - not(feature = "injective") - ))] - { - return Err(ContractError::TokenFactoryNotEnabled); - } - messages.push(white_whale_std::tokenfactory::create_denom::create_denom( env.contract.address, lp_symbol, diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index e17d0351..ce272500 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use cosmwasm_std::{coin, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256}; +use cosmwasm_std::{coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256}; use white_whale_std::pool_manager::{ AssetDecimalsResponse, Config, PoolInfoResponse, PoolType, ReverseSimulationResponse, @@ -304,9 +304,7 @@ pub fn simulate_swap_operations( operations: Vec, ) -> Result { let operations_len = operations.len(); - if operations_len == 0 { - return Err(ContractError::NoSwapOperationsProvided); - } + ensure!(operations_len > 0, ContractError::NoSwapOperationsProvided); let mut amount = offer_amount; diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index cfa5a9e7..ec81e488 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -4,16 +4,18 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ to_json_binary, Addr, Coin, CosmosMsg, Decimal, StdResult, Timestamp, Uint128, Uint64, WasmMsg, }; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; #[cw_serde] pub struct Config { - /// Owner of the contract. - pub owner: Addr, /// Pool Manager contract address for swapping pub pool_manager_addr: Addr, + /// Epoch Manager contract address + pub epoch_manager_addr: Addr, /// Distribution denom for the rewards pub distribution_denom: String, - /// Unbonding period in nanoseconds. + /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can + /// be withdrawn pub unbonding_period: Uint64, /// A fraction that controls the effect of time on the weight of a bond. If the growth rate is set /// to zero, time will have no impact on the weight. @@ -81,7 +83,8 @@ pub struct GlobalIndex { pub struct InstantiateMsg { /// Denom to be swapped to and rewarded pub distribution_denom: String, - /// Unbonding period in nanoseconds. + /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can + /// be withdrawn pub unbonding_period: Uint64, /// Weight grow rate. Needs to be between 0 and 1. pub growth_rate: Decimal, @@ -89,6 +92,8 @@ pub struct InstantiateMsg { pub bonding_assets: Vec, /// Grace period the maximum age of a bucket before fees are forwarded from it pub grace_period: Uint64, + /// The epoch manager contract + pub epoch_manager_addr: String, } #[cw_serde] @@ -96,85 +101,106 @@ pub struct EpochChangedHookMsg { pub current_epoch: EpochV2, } +#[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { /// Bonds the specified [Asset]. - Bond {}, + Bond, /// Unbonds the specified [Asset]. Unbond { + /// The asset to unbond. asset: Coin, }, /// Sends withdrawable unbonded tokens to the user. Withdraw { + /// The denom to withdraw. denom: String, }, /// Updates the [Config] of the contract. UpdateConfig { - owner: Option, + /// The new pool manager address. pool_manager_addr: Option, + /// The unbonding period. unbonding_period: Option, + /// The new growth rate. growth_rate: Option, }, - Claim {}, + /// Claims the available rewards + Claim, - /// Fills the whale lair with new rewards. + /// Fills the contract with new rewards. FillRewards, - /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in + /// the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) EpochChangedHook { + /// The current epoch, the one that was newly created. current_epoch: EpochV2, }, } +#[cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { /// Returns the [Config] of te contract. #[returns(Config)] - Config {}, + Config, /// Returns the amount of assets that have been bonded by the specified address. #[returns(BondedResponse)] - Bonded { address: String }, + Bonded { + /// The address to check for bonded assets. If none is provided, all bonded assets in the + /// 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)] Unbonding { + /// The address to check for unbonding assets. address: String, + /// The denom to check for unbonding assets. denom: String, + /// The amount of unbonding assets to skip. Allows pagination. start_after: Option, + /// 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)] - Withdrawable { address: String, denom: String }, + Withdrawable { + /// The address to check for withdrawable assets. + address: String, + /// The denom to check for withdrawable assets. + denom: String, + }, /// 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. timestamp: Option, + /// The global index to check for weight. If none is provided, the current global index is used. global_index: Option, }, - /// Returns the total amount of assets that have been bonded to the contract. - #[returns(BondedResponse)] - TotalBonded {}, - /// Returns the global index of the contract. #[returns(GlobalIndex)] - GlobalIndex {}, - - /// Returns the [Epoch]s that can be claimed. - #[returns(ClaimableEpochsResponse)] - ClaimableEpochs {}, + GlobalIndex, /// Returns the [Epoch]s that can be claimed by an address. #[returns(ClaimableEpochsResponse)] - Claimable { addr: String }, + Claimable { + /// The address to check for claimable epochs. If none is provided, all possible epochs + /// stored in the contract that can potentially be claimed are returned. + address: Option, + }, } #[cw_serde] @@ -183,31 +209,44 @@ pub struct MigrateMsg {} /// Response for the Bonded query #[cw_serde] pub struct BondedResponse { + /// The total amount of bonded tokens by the address. Bear in mind the bonded assets are + /// considered to be equal for this purpose. pub total_bonded: Uint128, + /// The total amount of bonded assets by the address. pub bonded_assets: Vec, - pub first_bonded_epoch_id: Uint64, + /// If Some, the epoch id at which the user/address bonded first time. None is used when this + /// Response is used to check the bonded assets in the contract. + pub first_bonded_epoch_id: Option, } /// Response for the Unbonding query #[cw_serde] pub struct UnbondingResponse { + /// The total amount of unbonded tokens by the address. pub total_amount: Uint128, + /// The total amount of unbonded assets by the address. pub unbonding_requests: Vec, } /// Response for the Withdrawable query #[cw_serde] pub struct WithdrawableResponse { + /// The total amount of withdrawable assets by the address. pub withdrawable_amount: Uint128, } /// Response for the Weight query. #[cw_serde] pub struct BondingWeightResponse { + /// The weight of the address. pub address: String, + /// The weight of the address at the given timestamp. pub weight: Uint128, + /// The global weight of the contract. pub global_weight: Uint128, + /// The share the address has of the rewards at the particular timestamp. pub share: Decimal, + /// The timestamp at which the weight was calculated. pub timestamp: Timestamp, } @@ -220,12 +259,8 @@ pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult, } diff --git a/packages/white-whale-std/src/constants.rs b/packages/white-whale-std/src/constants.rs index c8b46adb..45a14c1c 100644 --- a/packages/white-whale-std/src/constants.rs +++ b/packages/white-whale-std/src/constants.rs @@ -1,2 +1,2 @@ pub const LP_SYMBOL: &str = "uLP"; -pub const DAY_SECONDS: u64 = 86400u64; +pub const DAY_IN_SECONDS: u64 = 86_400u64; diff --git a/packages/white-whale-std/src/epoch_manager/common.rs b/packages/white-whale-std/src/epoch_manager/common.rs index 7e065d10..ce067db2 100644 --- a/packages/white-whale-std/src/epoch_manager/common.rs +++ b/packages/white-whale-std/src/epoch_manager/common.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Deps, StdError, StdResult, Timestamp}; -use crate::constants::DAY_SECONDS; +use crate::constants::DAY_IN_SECONDS; use crate::epoch_manager::epoch_manager::{Epoch, EpochResponse, QueryMsg}; /// Queries the current epoch from the epoch manager contract @@ -17,7 +17,7 @@ pub fn validate_epoch(epoch: &Epoch, current_time: Timestamp) -> StdResult<()> { if current_time .minus_seconds(epoch.start_time.seconds()) .seconds() - < DAY_SECONDS + < DAY_IN_SECONDS { return Err(StdError::generic_err( "Current epoch has expired, please wait for the next epoch to start.", From 4a3649214ae4a6d1413e48976663b3c64772a17b Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 8 May 2024 17:17:31 +0100 Subject: [PATCH 21/51] test(bonding-manager): fix instantiate tests --- .../schema/bonding-manager.json | 7 ++ .../bonding-manager/schema/raw/execute.json | 7 ++ .../bonding-manager/src/commands.rs | 6 ++ .../bonding-manager/src/contract.rs | 10 +- .../bonding-manager/src/tests/bond.rs | 4 +- .../bonding-manager/src/tests/claim.rs | 6 +- .../bonding-manager/src/tests/instantiate.rs | 56 +++++++---- .../bonding-manager/src/tests/mod.rs | 16 +-- .../bonding-manager/src/tests/rewards.rs | 4 +- .../src/tests/{robot.rs => suite.rs} | 99 +++++-------------- .../bonding-manager/src/tests/unbond.rs | 10 +- .../src/tests/update_config.rs | 6 +- .../bonding-manager/src/tests/withdraw.rs | 6 +- .../white-whale-std/src/bonding_manager.rs | 2 + 14 files changed, 114 insertions(+), 125 deletions(-) rename contracts/liquidity_hub/bonding-manager/src/tests/{robot.rs => suite.rs} (89%) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index bb3a525c..128697b6 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -138,6 +138,13 @@ "update_config": { "type": "object", "properties": { + "epoch_manager_addr": { + "description": "The new epoch manager address.", + "type": [ + "string", + "null" + ] + }, "growth_rate": { "description": "The new growth rate.", "anyOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index 9f165688..9fceba5b 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -69,6 +69,13 @@ "update_config": { "type": "object", "properties": { + "epoch_manager_addr": { + "description": "The new epoch manager address.", + "type": [ + "string", + "null" + ] + }, "growth_rate": { "description": "The new growth rate.", "anyOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 2f6457fa..19d9ac58 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -179,6 +179,7 @@ pub(crate) fn withdraw( pub(crate) fn update_config( deps: DepsMut, info: MessageInfo, + epoch_manager_addr: Option, pool_manager_addr: Option, unbonding_period: Option, growth_rate: Option, @@ -188,6 +189,10 @@ pub(crate) fn update_config( let mut config = CONFIG.load(deps.storage)?; + if let Some(epoch_manager_addr) = epoch_manager_addr { + config.epoch_manager_addr = deps.api.addr_validate(&epoch_manager_addr)?; + } + if let Some(pool_manager_addr) = pool_manager_addr { config.pool_manager_addr = deps.api.addr_validate(&pool_manager_addr)?; } @@ -206,6 +211,7 @@ pub(crate) fn update_config( Ok(Response::default().add_attributes(vec![ ("action", "update_config".to_string()), ("pool_manager_addr", config.pool_manager_addr.to_string()), + ("epoch_manager_addr", config.epoch_manager_addr.to_string()), ("unbonding_period", config.unbonding_period.to_string()), ("growth_rate", config.growth_rate.to_string()), ])) diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 4cb8fc86..0cfd6c64 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -76,12 +76,20 @@ pub fn execute( commands::withdraw(deps, env.block.time, info.sender, denom) } ExecuteMsg::UpdateConfig { + epoch_manager_addr: epock_manager_addr, pool_manager_addr, unbonding_period, growth_rate, } => { cw_utils::nonpayable(&info)?; - commands::update_config(deps, info, pool_manager_addr, unbonding_period, growth_rate) + commands::update_config( + deps, + info, + epock_manager_addr, + pool_manager_addr, + unbonding_period, + growth_rate, + ) } ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), ExecuteMsg::Claim => commands::claim(deps, info), diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index ed5f7d40..a296852d 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -2,13 +2,13 @@ use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse}; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; use super::test_helpers::get_epochs; #[test] fn test_bond_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); let another_sender = robot.another_sender.clone(); get_epochs(); diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs index a6eb7700..373f775d 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{coin, Uint64}; use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; use crate::tests::test_helpers; use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; @@ -15,7 +15,7 @@ use super::test_helpers::get_epochs; #[test] fn test_claimable_epochs() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let grace_period = Uint64::new(21); let creator = robot.sender.clone(); @@ -171,7 +171,7 @@ fn test_claimable_epochs() { #[test] fn test_claim_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); let another_sender = robot.another_sender.clone(); let asset_infos = vec!["uwhale".to_string(), "uusdc".to_string()]; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index 0ad6aa84..9e7fdf77 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -1,13 +1,14 @@ use cosmwasm_std::{Addr, Decimal, Uint64}; +use white_whale_std::bonding_manager::Config; + use crate::state::BONDING_ASSETS_LIMIT; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; use crate::ContractError; -use white_whale_std::bonding_manager::Config; #[test] fn test_instantiate_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); robot .instantiate( @@ -17,8 +18,8 @@ fn test_instantiate_successfully() { &vec![], ) .assert_config(Config { - owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000u64), growth_rate: Decimal::one(), @@ -29,23 +30,36 @@ fn test_instantiate_successfully() { #[test] fn test_instantiate_unsuccessfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); // over bonding assets limit - robot.instantiate_err( - Uint64::new(1_000u64), - Decimal::one(), - vec![ - "ampWHALE".to_string(), - "bWHALE".to_string(), - "uwhale".to_string(), - ], - &vec![], - |error| { - assert_eq!( - error.root_cause().downcast_ref::().unwrap(), - &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3) - ); - }, - ); + robot + .instantiate_err( + Uint64::new(1_000u64), + Decimal::one(), + vec![ + "ampWHALE".to_string(), + "bWHALE".to_string(), + "uwhale".to_string(), + ], + &vec![], + |error| { + assert_eq!( + error.root_cause().downcast_ref::().unwrap(), + &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3) + ); + }, + ) + .instantiate_err( + Uint64::new(1_000u64), + Decimal::percent(200), + vec!["ampWHALE".to_string(), "bWHALE".to_string()], + &vec![], + |error| { + assert_eq!( + error.root_cause().downcast_ref::().unwrap(), + &ContractError::InvalidGrowthRate + ); + }, + ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index c5f0b05e..9d0f1f2c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -1,9 +1,9 @@ -mod bond; -mod claim; +// mod bond; +// mod claim; mod instantiate; -mod rewards; -mod robot; -mod test_helpers; -mod unbond; -mod update_config; -mod withdraw; +// mod rewards; +mod suite; +// mod test_helpers; +// mod unbond; +// mod update_config; +// mod withdraw; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs index 143289a2..d5243862 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs @@ -5,12 +5,12 @@ use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_manager::SwapRoute; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; use crate::tests::test_helpers; #[test] fn test_fill_rewards_from_pool_manager() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let creator = robot.sender.clone(); let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs similarity index 89% rename from contracts/liquidity_hub/bonding-manager/src/tests/robot.rs rename to contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 7f848598..c8f08e4a 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -11,7 +11,6 @@ use cw_multi_test::{ use white_whale_std::fee::PoolFee; use white_whale_testing::multi_test::stargate_mock::StargateMock; -use crate::contract::query; use crate::state::{CONFIG, EPOCHS}; use cw_multi_test::{Contract, ContractWrapper}; use white_whale_std::bonding_manager::{ @@ -70,7 +69,7 @@ type OsmosisTokenFactoryApp = App< GovFailingModule, StargateMock, >; -pub struct TestingRobot { +pub struct TestingSuite { pub app: OsmosisTokenFactoryApp, pub sender: Addr, pub another_sender: Addr, @@ -82,7 +81,7 @@ pub struct TestingRobot { } /// instantiate / execute messages -impl TestingRobot { +impl TestingSuite { pub(crate) fn default() -> Self { let sender = Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); let another_sender = Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"); @@ -151,10 +150,7 @@ impl TestingRobot { funds: &Vec, ) -> &mut Self { let epoch_manager_id = self.app.store_code(epoch_manager_contract()); - println!( - "epoch_manager_id: {}", - self.app.block_info().time.minus_seconds(10).nanos() - ); + println!("epoch_manager_id: {}", self.app.block_info().time.nanos()); let epoch_manager_addr = self .app .instantiate_contract( @@ -163,11 +159,11 @@ impl TestingRobot { &white_whale_std::epoch_manager::epoch_manager::InstantiateMsg { start_epoch: EpochV2 { id: 0, - start_time: self.app.block_info().time.plus_seconds(10), + start_time: self.app.block_info().time, }, epoch_config: EpochConfig { duration: Uint64::new(86_400_000_000_000u64), // a day - genesis_epoch: self.app.block_info().time.plus_seconds(10).nanos().into(), // March 14, 2023 2:00:00 PM + genesis_epoch: self.app.block_info().time.nanos().into(), }, }, &[], @@ -195,8 +191,6 @@ impl TestingRobot { ) .unwrap(); - println!("hook_registration_msg: {:?}", resp); - let msg = white_whale_std::pool_manager::InstantiateMsg { bonding_manager_addr: bonding_manager_addr.clone().to_string(), incentive_manager_addr: bonding_manager_addr.clone().to_string(), @@ -222,9 +216,9 @@ impl TestingRobot { ) .unwrap(); let msg = ExecuteMsg::UpdateConfig { + epoch_manager_addr: Some(epoch_manager_addr.clone().to_string()), pool_manager_addr: Some(pool_manager_addr.clone().to_string()), growth_rate: None, - owner: None, unbonding_period: None, }; self.app @@ -349,12 +343,13 @@ impl TestingRobot { sender: Addr, owner: Option, pool_manager_addr: Option, + epoch_manager_addr: Option, unbonding_period: Option, growth_rate: Option, response: impl Fn(Result), ) -> &mut Self { let msg = ExecuteMsg::UpdateConfig { - owner, + epoch_manager_addr, pool_manager_addr, unbonding_period, growth_rate, @@ -384,7 +379,7 @@ impl TestingRobot { } fn instantiate_contract( - robot: &mut TestingRobot, + robot: &mut TestingSuite, unbonding_period: Uint64, growth_rate: Decimal, bonding_assets: Vec, @@ -396,6 +391,7 @@ fn instantiate_contract( growth_rate, bonding_assets, grace_period: Uint64::new(21), + epoch_manager_addr: "".to_string(), }; let bonding_manager_id = robot.app.store_code(bonding_manager_contract()); @@ -410,7 +406,7 @@ fn instantiate_contract( } /// queries -impl TestingRobot { +impl TestingSuite { pub(crate) fn query_config( &mut self, response: impl Fn(StdResult<(&mut Self, Config)>), @@ -455,53 +451,17 @@ impl TestingRobot { address: Option, response: impl Fn(StdResult<(&mut Self, Vec)>), ) -> &mut Self { - let query_res: ClaimableEpochsResponse = if let Some(address) = address { - self.app - .wrap() - .query_wasm_smart( - &self.bonding_manager_addr, - &QueryMsg::Claimable { - addr: address.to_string(), - }, - ) - .unwrap() + let address = if let Some(address) = address { + Some(address.to_string()) } else { - self.app - .wrap() - .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::ClaimableEpochs {}) - .unwrap() + None }; - response(Ok((self, query_res.epochs))); - - self - } - - pub(crate) fn query_claimable_epochs_live( - &mut self, - address: Option, - response: impl Fn(StdResult<(&mut Self, Vec)>), - ) -> &mut Self { - let query_res = if let Some(address) = address { - let bonded_response: ClaimableEpochsResponse = self - .app - .wrap() - .query_wasm_smart( - &self.bonding_manager_addr, - &QueryMsg::Claimable { - addr: address.to_string(), - }, - ) - .unwrap(); - bonded_response - } else { - let bonded_response: ClaimableEpochsResponse = self - .app - .wrap() - .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::ClaimableEpochs {}) - .unwrap(); - bonded_response - }; + let query_res: ClaimableEpochsResponse = self + .app + .wrap() + .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::Claimable { address }) + .unwrap(); response(Ok((self, query_res.epochs))); @@ -510,7 +470,7 @@ impl TestingRobot { pub(crate) fn query_bonded( &mut self, - address: String, + address: Option, response: impl Fn(StdResult<(&mut Self, BondedResponse)>), ) -> &mut Self { let bonded_response: BondedResponse = self @@ -572,21 +532,6 @@ impl TestingRobot { self } - pub(crate) fn query_total_bonded( - &mut self, - response: impl Fn(StdResult<(&mut Self, BondedResponse)>), - ) -> &mut Self { - let bonded_response: BondedResponse = self - .app - .wrap() - .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::TotalBonded {}) - .unwrap(); - - response(Ok((self, bonded_response))); - - self - } - // Pool Manager methods #[track_caller] @@ -725,7 +670,7 @@ impl TestingRobot { } /// assertions -impl TestingRobot { +impl TestingSuite { pub(crate) fn assert_config(&mut self, expected: Config) -> &mut Self { self.query_config(|res| { let config = res.unwrap().1; @@ -740,7 +685,7 @@ impl TestingRobot { address: String, expected: BondedResponse, ) -> &mut Self { - self.query_bonded(address, |res| { + self.query_bonded(Some(address), |res| { let bonded_response = res.unwrap().1; assert_eq!(bonded_response, expected); }) diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs index 1a6f3c75..06584511 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/unbond.rs @@ -4,12 +4,12 @@ use white_whale_std::bonding_manager::{ Bond, BondedResponse, BondingWeightResponse, UnbondingResponse, }; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; #[test] #[track_caller] fn test_unbond_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); let another_sender = robot.another_sender.clone(); @@ -160,7 +160,7 @@ fn test_unbond_successfully() { #[test] fn test_unbond_all_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); robot @@ -200,7 +200,7 @@ fn test_unbond_all_successfully() { #[test] #[track_caller] fn test_unbonding_query_pagination() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); robot @@ -378,7 +378,7 @@ fn test_unbonding_query_pagination() { #[test] fn test_unbond_unsuccessfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); robot diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index a655db2e..e2b39143 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -2,11 +2,11 @@ use cosmwasm_std::{Addr, Decimal, Uint128, Uint64}; use white_whale_std::bonding_manager::Config; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; #[test] fn test_update_config_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let owner = robot.sender.clone(); robot @@ -61,7 +61,7 @@ fn test_update_config_successfully() { #[test] fn test_update_config_unsuccessfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); robot .instantiate_default() diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs b/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs index fed6f19e..0abf1d75 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/withdraw.rs @@ -2,11 +2,11 @@ use cosmwasm_std::{coins, Coin, Event, Uint128}; use white_whale_std::bonding_manager::WithdrawableResponse; -use crate::tests::robot::TestingRobot; +use crate::tests::suite::TestingSuite; #[test] fn test_withdraw_successfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); let another_sender = robot.another_sender.clone(); @@ -64,7 +64,7 @@ fn test_withdraw_successfully() { #[test] fn test_withdraw_unsuccessfully() { - let mut robot = TestingRobot::default(); + let mut robot = TestingSuite::default(); let sender = robot.sender.clone(); let another_sender = robot.another_sender.clone(); diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index ec81e488..61203e62 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -118,6 +118,8 @@ pub enum ExecuteMsg { }, /// Updates the [Config] of the contract. UpdateConfig { + /// The new epoch manager address. + epoch_manager_addr: Option, /// The new pool manager address. pool_manager_addr: Option, /// The unbonding period. From c364fc1c56f5a606366c795a198c8f2bcecae003 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 8 May 2024 17:29:04 +0100 Subject: [PATCH 22/51] test(bonding-manager): fix update config tests --- .../bonding-manager/src/contract.rs | 4 +- .../bonding-manager/src/tests/mod.rs | 3 +- .../bonding-manager/src/tests/suite.rs | 27 ++++- .../bonding-manager/src/tests/test_helpers.rs | 109 ------------------ .../src/tests/update_config.rs | 51 ++++---- 5 files changed, 57 insertions(+), 137 deletions(-) delete mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/test_helpers.rs diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 0cfd6c64..233a55ab 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -76,7 +76,7 @@ pub fn execute( commands::withdraw(deps, env.block.time, info.sender, denom) } ExecuteMsg::UpdateConfig { - epoch_manager_addr: epock_manager_addr, + epoch_manager_addr, pool_manager_addr, unbonding_period, growth_rate, @@ -85,7 +85,7 @@ pub fn execute( commands::update_config( deps, info, - epock_manager_addr, + epoch_manager_addr, pool_manager_addr, unbonding_period, growth_rate, diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index 9d0f1f2c..92ba66f5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -3,7 +3,6 @@ mod instantiate; // mod rewards; mod suite; -// mod test_helpers; // mod unbond; -// mod update_config; +mod update_config; // mod withdraw; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index c8f08e4a..9c0f1f61 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -341,9 +341,8 @@ impl TestingSuite { pub(crate) fn update_config( &mut self, sender: Addr, - owner: Option, - pool_manager_addr: Option, epoch_manager_addr: Option, + pool_manager_addr: Option, unbonding_period: Option, growth_rate: Option, response: impl Fn(Result), @@ -422,6 +421,21 @@ impl TestingSuite { self } + pub(crate) fn query_owner( + &mut self, + response: impl Fn(StdResult<(&mut Self, String)>), + ) -> &mut Self { + let ownership: cw_ownable::Ownership = self + .app + .wrap() + .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::Ownership {}) + .unwrap(); + + response(Ok((self, ownership.owner.unwrap()))); + + self + } + pub(crate) fn query_weight( &mut self, address: String, @@ -680,6 +694,15 @@ impl TestingSuite { self } + pub(crate) fn assert_owner(&mut self, expected: String) -> &mut Self { + self.query_owner(|res| { + let owner = res.unwrap().1; + assert_eq!(owner, expected); + }); + + self + } + pub(crate) fn assert_bonded_response( &mut self, address: String, diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/test_helpers.rs b/contracts/liquidity_hub/bonding-manager/src/tests/test_helpers.rs deleted file mode 100644 index aef315dc..00000000 --- a/contracts/liquidity_hub/bonding-manager/src/tests/test_helpers.rs +++ /dev/null @@ -1,109 +0,0 @@ -use cosmwasm_std::{Coin, Timestamp, Uint128, Uint64}; - -use white_whale_std::bonding_manager::Epoch; -use white_whale_std::bonding_manager::GlobalIndex; - -// TODO: might remove currently unused -pub(crate) fn get_epochs() -> Vec { - vec![ - Epoch { - global_index: GlobalIndex { - weight: Uint128::from(1u128), - bonded_amount: Default::default(), - bonded_assets: vec![], - timestamp: Default::default(), - }, - id: Uint64::new(1u64), - start_time: Timestamp::from_seconds(1678726800), - total: vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(10_000_000u128), - }, - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(10_000_000u128), - }, - ], - available: vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000_000u128), - }, - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(7_000_000u128), - }, - ], - claimed: vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(9_000_000u128), - }, - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(3_000_000u128), - }, - ], - }, - Epoch { - global_index: GlobalIndex { - weight: Uint128::from(1u128), - bonded_amount: Default::default(), - bonded_assets: vec![], - timestamp: Default::default(), - }, - id: Uint64::new(2u64), - start_time: Timestamp::from_seconds(1678813200), - total: vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(15_000_000u128), - }], - available: vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(15_000_000u128), - }], - claimed: vec![], - }, - Epoch { - global_index: GlobalIndex { - weight: Uint128::from(1u128), - bonded_amount: Default::default(), - bonded_assets: vec![], - timestamp: Default::default(), - }, - id: Uint64::new(3u64), - start_time: Timestamp::from_seconds(1678899600), - total: vec![ - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(5_000_000u128), - }, - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(5_000_000u128), - }, - ], - available: vec![ - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(4_000_000u128), - }, - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(4_000_000u128), - }, - ], - claimed: vec![ - Coin { - denom: "uatom".to_string(), - amount: Uint128::from(1_000_000u128), - }, - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000_000u128), - }, - ], - }, - ] -} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index e2b39143..c0a65290 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{Addr, Decimal, Uint128, Uint64}; +use crate::ContractError; use white_whale_std::bonding_manager::Config; use crate::tests::suite::TestingSuite; @@ -12,14 +13,15 @@ fn test_update_config_successfully() { robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), growth_rate: Decimal::one(), grace_period: Uint64::new(21u64), bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) + .assert_owner("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3".to_string()) .update_config( owner.clone(), None, @@ -32,8 +34,8 @@ fn test_update_config_successfully() { |_res| {}, ) .assert_config(Config { - owner: owner.clone(), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(500u64), growth_rate: Decimal::from_ratio(Uint128::new(1u128), Uint128::new(2u128)), @@ -42,15 +44,15 @@ fn test_update_config_successfully() { }) .update_config( owner, - Some("new_owner".to_string()), - None, + Some("contract5".to_string()), + Some("contract6".to_string()), None, Some(Decimal::one()), |_res| {}, ) .assert_config(Config { - owner: Addr::unchecked("new_owner"), - pool_manager_addr: Addr::unchecked("contract2"), + pool_manager_addr: Addr::unchecked("contract6"), + epoch_manager_addr: Addr::unchecked("contract5"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(500u64), growth_rate: Decimal::one(), @@ -62,12 +64,13 @@ fn test_update_config_successfully() { #[test] fn test_update_config_unsuccessfully() { let mut robot = TestingSuite::default(); + let owner = robot.sender.clone(); robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), growth_rate: Decimal::one(), @@ -83,17 +86,19 @@ fn test_update_config_unsuccessfully() { Uint128::new(1u128), Uint128::new(2u128), )), - |_res| { - //println!("{:?}", res.unwrap_err().root_cause()); - // assert_eq!( - // res.unwrap_err().root_cause().downcast_ref::().unwrap(), - // &ContractError::Unauthorized {} - // ); + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::OwnershipError { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::OwnershipError") + } + } }, ) .assert_config(Config { - owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), growth_rate: Decimal::one(), @@ -101,7 +106,7 @@ fn test_update_config_unsuccessfully() { bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) .update_config( - Addr::unchecked("owner"), + owner, None, None, Some(Uint64::new(500u64)), @@ -109,17 +114,19 @@ fn test_update_config_unsuccessfully() { Uint128::new(2u128), Uint128::new(1u128), )), - |_res| { - //println!("{:?}", res.unwrap_err().root_cause()); - // assert_eq!( - // res.unwrap_err().root_cause().downcast_ref::().unwrap(), - // &ContractError::Unauthorized {} - // ); + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidGrowthRate { .. } => {} + _ => { + panic!("Wrong error type, should return ContractError::InvalidGrowthRate") + } + } }, ) .assert_config(Config { - owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), growth_rate: Decimal::one(), From 1f3acee76071467a3c4e9ec4ac257bc5788f38ca Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 9 May 2024 11:51:23 +0100 Subject: [PATCH 23/51] test: fix bond tests --- .../schema/bonding-manager.json | 9 ++ .../schema/raw/response_to_unbonding.json | 9 ++ .../bonding-manager/src/commands.rs | 20 ++- .../bonding-manager/src/helpers.rs | 17 ++- .../bonding-manager/src/queries.rs | 33 +--- .../bonding-manager/src/tests/bond.rs | 144 ++++++++++++------ .../bonding-manager/src/tests/instantiate.rs | 8 +- .../bonding-manager/src/tests/mod.rs | 2 +- .../bonding-manager/src/tests/suite.rs | 11 +- .../src/tests/update_config.rs | 12 +- .../white-whale-std/src/bonding_manager.rs | 3 + 11 files changed, 181 insertions(+), 87 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 128697b6..e377acb2 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -1158,6 +1158,7 @@ "type": "object", "required": [ "asset", + "created_at_epoch", "timestamp", "weight" ], @@ -1170,6 +1171,14 @@ } ] }, + "created_at_epoch": { + "description": "The epoch id at which the Bond was created.", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, "timestamp": { "description": "The timestamp at which the bond was done.", "allOf": [ 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 476b888b..1582163d 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 @@ -30,6 +30,7 @@ "type": "object", "required": [ "asset", + "created_at_epoch", "timestamp", "weight" ], @@ -42,6 +43,14 @@ } ] }, + "created_at_epoch": { + "description": "The epoch id at which the Bond was created.", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, "timestamp": { "description": "The timestamp at which the bond was done.", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 19d9ac58..5f74cb86 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -23,9 +23,18 @@ pub(crate) fn bond( asset: Coin, ) -> Result { println!("bonding"); + helpers::validate_epochs(&deps)?; helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps, &env)?; println!("bonding 2"); + + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + let mut bond = BOND .key((&info.sender, &asset.denom)) .may_load(deps.storage)? @@ -34,6 +43,7 @@ pub(crate) fn bond( amount: Uint128::zero(), ..asset.clone() }, + created_at_epoch: Uint64::new(current_epoch.epoch.id), ..Bond::default() }); @@ -97,6 +107,14 @@ pub(crate) fn unbond( } else { BOND.save(deps.storage, (&info.sender, &asset.denom), &unbond)?; } + + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + // record the unbonding UNBOND.save( deps.storage, @@ -105,6 +123,7 @@ pub(crate) fn unbond( asset: asset.clone(), weight: Uint128::zero(), timestamp, + created_at_epoch: Uint64::new(current_epoch.epoch.id), }, )?; // update global values @@ -149,7 +168,6 @@ pub(crate) fn withdraw( for unbonding in unbondings { let (ts, bond) = unbonding; if timestamp.minus_nanos(config.unbonding_period.u64()) >= bond.timestamp { - // TODO: Clean up the bond asset let denom = bond.asset.denom; refund_amount = refund_amount.checked_add(bond.asset.amount)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 9043699a..67cbe51c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, ReplyOn, + ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, ReplyOn, StdResult, SubMsg, Timestamp, Uint64, WasmMsg, }; use cw_utils::PaymentError; @@ -13,7 +13,7 @@ use white_whale_std::pool_manager::{ use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; use crate::queries::query_claimable; -use crate::state::CONFIG; +use crate::state::{CONFIG, EPOCHS}; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { @@ -99,6 +99,7 @@ pub fn validate_bonding_for_current_epoch(deps: &DepsMut, env: &Env) -> Result<( Ok(()) } +//todo remove /// Calculates the epoch id for any given timestamp based on the genesis epoch configuration. pub fn calculate_epoch( genesis_epoch_config: EpochConfig, @@ -284,6 +285,18 @@ pub fn swap_coins_to_main_token( Ok(()) } +/// Validates that there are epochs in the state. If there are none, it means the system has just +/// been started and the epoch manager has still not created any epochs yet. +pub(crate) fn validate_epochs(deps: &DepsMut) -> Result<(), ContractError> { + let epochs = EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .collect::>>()?; + + ensure!(!epochs.is_empty(), ContractError::Unauthorized); + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 61e59647..05eacdb2 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,10 +1,6 @@ use std::collections::{HashSet, VecDeque}; -use white_whale_std::epoch_manager::epoch_manager::ConfigResponse; -use cosmwasm_std::{ - to_json_binary, Decimal, Deps, Order, QueryRequest, StdError, StdResult, Timestamp, Uint128, - Uint64, WasmQuery, -}; +use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Timestamp, Uint128, Uint64}; use cw_storage_plus::Bound; use white_whale_std::bonding_manager::{ @@ -12,9 +8,7 @@ use white_whale_std::bonding_manager::{ WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; -use white_whale_std::epoch_manager::epoch_manager::QueryMsg; -use crate::helpers; use crate::state::{ get_weight, BOND, BONDING_ASSETS_LIMIT, CONFIG, EPOCHS, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, }; @@ -43,38 +37,25 @@ pub(crate) fn query_bonded(deps: Deps, address: Option) -> StdResult().unwrap(); + match err { + // no epochs has been created yet + ContractError::Unauthorized => {} + _ => { + panic!("Wrong error type, should return ContractError::Unauthorized") + } + } + }, + ) + .assert_bonded_response( + sender.to_string(), + BondedResponse { + total_bonded: Default::default(), + bonded_assets: Default::default(), + first_bonded_epoch_id: None, + }, + ); + + suite + .add_one_day() + .create_new_epoch() + .bond( + sender.clone(), + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + &coins(1_000u128, "ampWHALE"), + |result| { + result.unwrap(); + }, ) .assert_bonded_response( sender.to_string(), @@ -31,21 +62,22 @@ fn test_bond_successfully() { denom: "ampWHALE".to_string(), amount: Uint128::new(1_000u128), }], - first_bonded_epoch_id: Default::default(), + first_bonded_epoch_id: Some(Uint64::new(1u64)), }, ) - .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), + weight: Uint128::new(1_000u128), + global_weight: Uint128::new(1_000u128), share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797429879305533u64), + timestamp: Timestamp::from_nanos(1571883819879305533u64), }, - ) - .fast_forward(10u64) + ); + + suite + .fast_forward(43_200u64) .bond( sender.clone(), Coin { @@ -53,7 +85,9 @@ fn test_bond_successfully() { amount: Uint128::new(3_000u128), }, &coins(3_000u128, "bWHALE"), - |_res| {}, + |result| { + result.unwrap(); + }, ) .assert_bonded_response( sender.to_string(), @@ -69,20 +103,13 @@ fn test_bond_successfully() { amount: Uint128::new(3_000u128), }, ], - first_bonded_epoch_id: Default::default(), + first_bonded_epoch_id: Some(Uint64::new(1u64)), }, - ) - .fast_forward(10u64) - // .assert_bonding_weight_response( - // sender.to_string(), - // BondingWeightResponse { - // address: sender.to_string(), - // weight: Uint128::new(64_000u128), - // global_weight: Uint128::new(64_000u128), - // share: Decimal::one(), - // timestamp: Timestamp::from_nanos(1571797449879305533u64), - // }, - // ) + ); + + suite + .add_one_day() + .create_new_epoch() .bond( another_sender.clone(), Coin { @@ -92,28 +119,28 @@ fn test_bond_successfully() { &coins(5_000u128, "ampWHALE"), |_res| {}, ) - .fast_forward(10u64) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(104_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(104_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), + .assert_bonded_response( + another_sender.to_string(), + BondedResponse { + total_bonded: Uint128::new(5_000u128), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(5_000u128), + }], + first_bonded_epoch_id: Some(Uint64::new(2u64)), }, ) .assert_bonding_weight_response( another_sender.to_string(), BondingWeightResponse { address: another_sender.to_string(), - weight: Uint128::new(55_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(55_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), + weight: Uint128::new(5_000u128), + global_weight: Uint128::new(950_409_000u128), + share: Decimal::from_ratio(5_000u128, 950_409_000u128), + timestamp: Timestamp::from_nanos(1572013419879305533u64), }, ) - .query_total_bonded(|res| { + .query_bonded(None, |res| { let bonded_response = res.unwrap().1; assert_eq!( bonded_response, @@ -129,8 +156,35 @@ fn test_bond_successfully() { amount: Uint128::new(3_000u128), }, ], - first_bonded_epoch_id: Default::default(), + first_bonded_epoch_id: None, } ) }); + + suite + .add_one_day() + .assert_bonding_weight_response( + sender.to_string(), + BondingWeightResponse { + address: sender.to_string(), + weight: Uint128::new(734_404_000u128), + global_weight: Uint128::new(1_728_009_000u128), + share: Decimal::from_ratio(734_404_000u128, 1_728_009_000u128), + timestamp: Timestamp::from_nanos(1572099819879305533u64), + }, + ) + .assert_bonding_weight_response( + another_sender.to_string(), + BondingWeightResponse { + address: another_sender.to_string(), + weight: Uint128::new(432_005_000u128), + global_weight: Uint128::new(1_728_009_000u128), + share: Decimal::from_ratio(432_005_000u128, 1_728_009_000u128), + timestamp: Timestamp::from_nanos(1572099819879305533u64), + }, + ) + .query_bonded(None, |result| { + let res = result.unwrap().1; + println!("{:?}", res); + }); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index 9e7fdf77..f02c7cab 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -8,9 +8,9 @@ use crate::ContractError; #[test] fn test_instantiate_successfully() { - let mut robot = TestingSuite::default(); + let mut suite = TestingSuite::default(); - robot + suite .instantiate( Uint64::new(1_000u64), Decimal::one(), @@ -30,10 +30,10 @@ fn test_instantiate_successfully() { #[test] fn test_instantiate_unsuccessfully() { - let mut robot = TestingSuite::default(); + let mut suite = TestingSuite::default(); // over bonding assets limit - robot + suite .instantiate_err( Uint64::new(1_000u64), Decimal::one(), diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index 92ba66f5..ca4721da 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -1,4 +1,4 @@ -// mod bond; +mod bond; // mod claim; mod instantiate; // mod rewards; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 9c0f1f61..6ea88c28 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -133,9 +133,17 @@ impl TestingSuite { self } + pub(crate) fn add_one_day(&mut self) -> &mut Self { + let mut block_info = self.app.block_info(); + block_info.time = block_info.time.plus_days(1); + self.app.set_block(block_info); + + self + } + pub(crate) fn instantiate_default(&mut self) -> &mut Self { self.instantiate( - Uint64::new(1_000_000_000_000u64), + Uint64::new(86_400_000000000u64), Decimal::one(), vec!["ampWHALE".to_string(), "bWHALE".to_string()], &vec![], @@ -439,7 +447,6 @@ impl TestingSuite { pub(crate) fn query_weight( &mut self, address: String, - response: impl Fn(StdResult<(&mut Self, BondingWeightResponse)>), ) -> &mut Self { let bonding_weight_response: BondingWeightResponse = self diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index c0a65290..58175cc3 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -7,10 +7,10 @@ use crate::tests::suite::TestingSuite; #[test] fn test_update_config_successfully() { - let mut robot = TestingSuite::default(); - let owner = robot.sender.clone(); + let mut suite = TestingSuite::default(); + let owner = suite.sender.clone(); - robot + suite .instantiate_default() .assert_config(Config { pool_manager_addr: Addr::unchecked("contract2"), @@ -63,10 +63,10 @@ fn test_update_config_successfully() { #[test] fn test_update_config_unsuccessfully() { - let mut robot = TestingSuite::default(); - let owner = robot.sender.clone(); + let mut suite = TestingSuite::default(); + let owner = suite.sender.clone(); - robot + suite .instantiate_default() .assert_config(Config { pool_manager_addr: Addr::unchecked("contract2"), diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 61203e62..355667aa 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -47,6 +47,8 @@ pub struct Epoch { pub struct Bond { /// The amount of bonded tokens. pub asset: Coin, + /// The epoch id at which the Bond was created. + pub created_at_epoch: Uint64, /// The timestamp at which the bond was done. pub timestamp: Timestamp, /// The weight of the bond at the given block height. @@ -60,6 +62,7 @@ impl Default for Bond { denom: String::new(), amount: Uint128::zero(), }, + created_at_epoch: Default::default(), timestamp: Timestamp::default(), weight: Uint128::zero(), } From 87aaccb73d8638ef4c78e467f4c5d596ade02da1 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 9 May 2024 13:14:59 +0100 Subject: [PATCH 24/51] refactor: update types, change weight calculation from timestamp to epoch based --- .../schema/bonding-manager.json | 230 ++++++------------ .../bonding-manager/schema/raw/execute.json | 14 +- .../schema/raw/instantiate.json | 24 +- .../bonding-manager/schema/raw/query.json | 46 ++-- .../schema/raw/response_to_bonded.json | 18 +- .../schema/raw/response_to_claimable.json | 18 +- .../schema/raw/response_to_config.json | 22 +- .../schema/raw/response_to_global_index.json | 26 +- .../schema/raw/response_to_unbonding.json | 34 +-- .../schema/raw/response_to_weight.json | 28 +-- .../bonding-manager/src/commands.rs | 69 +++--- .../bonding-manager/src/contract.rs | 33 ++- .../bonding-manager/src/queries.rs | 38 +-- .../bonding-manager/src/state.rs | 39 ++- .../bonding-manager/src/tests/bond.rs | 8 +- .../bonding-manager/src/tests/suite.rs | 2 +- .../white-whale-std/src/bonding_manager.rs | 42 ++-- 17 files changed, 274 insertions(+), 417 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index e377acb2..ee01742b 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -31,12 +31,10 @@ "type": "string" }, "grace_period": { - "description": "Grace period the maximum age of a bucket before fees are forwarded from it", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "growth_rate": { "description": "Weight grow rate. Needs to be between 0 and 1.", @@ -47,12 +45,10 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Unbonding period in epochs. The time (in epochs) that needs to pass before an unbonded position can be withdrawn", + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false, @@ -60,10 +56,6 @@ "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -165,14 +157,12 @@ }, "unbonding_period": { "description": "The unbonding period.", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -514,6 +504,15 @@ "description": "The address to check for weight.", "type": "string" }, + "epoch_id": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "global_index": { "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ @@ -524,17 +523,6 @@ "type": "null" } ] - }, - "timestamp": { - "description": "The timestamp to check for weight. If none is provided, the current block time is used.", - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -608,7 +596,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -627,13 +615,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", @@ -646,21 +632,9 @@ }, "additionalProperties": false }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -691,14 +665,12 @@ }, "first_bonded_epoch_id": { "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 }, "total_bonded": { "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", @@ -729,10 +701,6 @@ "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -796,7 +764,9 @@ "$ref": "#/definitions/GlobalIndex" }, "id": { - "$ref": "#/definitions/Uint64" + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "start_time": { "$ref": "#/definitions/Timestamp" @@ -815,7 +785,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -834,13 +804,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", @@ -905,12 +873,10 @@ ] }, "grace_period": { - "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "growth_rate": { "description": "A fraction that controls the effect of time on the weight of a bond. If the growth rate is set to zero, time will have no impact on the weight.", @@ -930,11 +896,9 @@ }, "unbonding_period": { "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false, @@ -946,10 +910,6 @@ "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -960,7 +920,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -979,13 +939,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", @@ -1013,21 +971,9 @@ } } }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -1159,7 +1105,7 @@ "required": [ "asset", "created_at_epoch", - "timestamp", + "updated_last", "weight" ], "properties": { @@ -1173,19 +1119,15 @@ }, "created_at_epoch": { "description": "The epoch id at which the Bond was created.", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "timestamp": { - "description": "The timestamp at which the bond was done.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "updated_last": { + "description": "The epoch id at which the bond was last time updated.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "weight": { "description": "The weight of the bond at the given block height.", @@ -1213,21 +1155,9 @@ } } }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } }, @@ -1238,9 +1168,9 @@ "type": "object", "required": [ "address", + "epoch_id", "global_weight", "share", - "timestamp", "weight" ], "properties": { @@ -1248,6 +1178,12 @@ "description": "The weight of the address.", "type": "string" }, + "epoch_id": { + "description": "The epoch id at which the weight was calculated.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "global_weight": { "description": "The global weight of the contract.", "allOf": [ @@ -1264,14 +1200,6 @@ } ] }, - "timestamp": { - "description": "The timestamp at which the weight was calculated.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] - }, "weight": { "description": "The weight of the address at the given timestamp.", "allOf": [ @@ -1287,21 +1215,9 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "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 9fceba5b..c6a562de 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -96,14 +96,12 @@ }, "unbonding_period": { "description": "The unbonding period.", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json index b0d74ffa..f93d08e6 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json @@ -27,12 +27,10 @@ "type": "string" }, "grace_period": { - "description": "Grace period the maximum age of a bucket before fees are forwarded from it", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "growth_rate": { "description": "Weight grow rate. Needs to be between 0 and 1.", @@ -43,12 +41,10 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Unbonding period in epochs. The time (in epochs) that needs to pass before an unbonded position can be withdrawn", + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false, @@ -56,10 +52,6 @@ "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index 096226bb..3c5ab6ae 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -123,6 +123,15 @@ "description": "The address to check for weight.", "type": "string" }, + "epoch_id": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "global_index": { "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ @@ -133,17 +142,6 @@ "type": "null" } ] - }, - "timestamp": { - "description": "The timestamp to check for weight. If none is provided, the current block time is used.", - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -217,7 +215,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -236,13 +234,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", @@ -255,21 +251,9 @@ }, "additionalProperties": false }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json index d8a0ddbd..6deb74cb 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json @@ -17,14 +17,12 @@ }, "first_bonded_epoch_id": { "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 }, "total_bonded": { "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", @@ -55,10 +53,6 @@ "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } 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 6a728ff2..41023177 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 @@ -58,7 +58,9 @@ "$ref": "#/definitions/GlobalIndex" }, "id": { - "$ref": "#/definitions/Uint64" + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "start_time": { "$ref": "#/definitions/Timestamp" @@ -77,7 +79,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -96,13 +98,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json index 3d6850fc..20a56f53 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json @@ -32,12 +32,10 @@ ] }, "grace_period": { - "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "growth_rate": { "description": "A fraction that controls the effect of time on the weight of a bond. If the growth rate is set to zero, time will have no impact on the weight.", @@ -57,11 +55,9 @@ }, "unbonding_period": { "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false, @@ -73,10 +69,6 @@ "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } 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 9b88c08c..4128e977 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 @@ -5,7 +5,7 @@ "required": [ "bonded_amount", "bonded_assets", - "timestamp", + "last_updated", "weight" ], "properties": { @@ -24,13 +24,11 @@ "$ref": "#/definitions/Coin" } }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "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.", @@ -58,21 +56,9 @@ } } }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } 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 1582163d..ffa51158 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", - "timestamp", + "updated_last", "weight" ], "properties": { @@ -45,19 +45,15 @@ }, "created_at_epoch": { "description": "The epoch id at which the Bond was created.", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "timestamp": { - "description": "The timestamp at which the bond was done.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] + "updated_last": { + "description": "The epoch id at which the bond was last time updated.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "weight": { "description": "The weight of the bond at the given block height.", @@ -85,21 +81,9 @@ } } }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json index 0e017e02..bbe32476 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json @@ -5,9 +5,9 @@ "type": "object", "required": [ "address", + "epoch_id", "global_weight", "share", - "timestamp", "weight" ], "properties": { @@ -15,6 +15,12 @@ "description": "The weight of the address.", "type": "string" }, + "epoch_id": { + "description": "The epoch id at which the weight was calculated.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "global_weight": { "description": "The global weight of the contract.", "allOf": [ @@ -31,14 +37,6 @@ } ] }, - "timestamp": { - "description": "The timestamp at which the weight was calculated.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] - }, "weight": { "description": "The weight of the address at the given timestamp.", "allOf": [ @@ -54,21 +52,9 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" } } } diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 5f74cb86..63441446 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, - StdError, StdResult, SubMsg, Timestamp, Uint128, Uint64, + StdError, StdResult, SubMsg, Uint128, Uint64, }; use white_whale_std::bonding_manager::{Bond, Epoch, GlobalIndex}; @@ -17,7 +17,6 @@ use crate::{helpers, ContractError}; /// Bonds the provided asset. pub(crate) fn bond( mut deps: DepsMut, - timestamp: Timestamp, info: MessageInfo, env: Env, asset: Coin, @@ -43,14 +42,14 @@ pub(crate) fn bond( amount: Uint128::zero(), ..asset.clone() }, - created_at_epoch: Uint64::new(current_epoch.epoch.id), + created_at_epoch: current_epoch.epoch.id, ..Bond::default() }); // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; - bond = update_local_weight(&mut deps, info.sender.clone(), timestamp, bond)?; + bond = update_local_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; BOND.save(deps.storage, (&info.sender, &asset.denom), &bond)?; // update global values @@ -60,7 +59,7 @@ pub(crate) fn bond( global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; global_index.bonded_assets = asset::aggregate_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index = update_global_weight(&mut deps, timestamp, global_index)?; + global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; GLOBAL.save(deps.storage, &global_index)?; @@ -74,7 +73,6 @@ pub(crate) fn bond( /// Unbonds the provided amount of tokens pub(crate) fn unbond( mut deps: DepsMut, - timestamp: Timestamp, info: MessageInfo, env: Env, asset: Coin, @@ -96,8 +94,20 @@ 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + // update local values, decrease the bond - unbond = update_local_weight(&mut deps, info.sender.clone(), timestamp, unbond.clone())?; + unbond = update_local_weight( + &mut deps, + info.sender.clone(), + current_epoch.epoch.id, + unbond.clone(), + )?; let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); unbond.weight = unbond.weight.saturating_sub(weight_slash); unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); @@ -108,27 +118,20 @@ pub(crate) fn unbond( BOND.save(deps.storage, (&info.sender, &asset.denom), &unbond)?; } - 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; - // record the unbonding UNBOND.save( deps.storage, - (&info.sender, &asset.denom, timestamp.nanos()), + (&info.sender, &asset.denom, env.block.time.nanos()), &Bond { asset: asset.clone(), weight: Uint128::zero(), - timestamp, - created_at_epoch: Uint64::new(current_epoch.epoch.id), + updated_last: current_epoch.epoch.id, + created_at_epoch: current_epoch.epoch.id, }, )?; // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - global_index = update_global_weight(&mut deps, timestamp, global_index)?; + global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); global_index.bonded_assets = white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; @@ -149,25 +152,28 @@ pub(crate) fn unbond( /// Withdraws the rewards for the provided address pub(crate) fn withdraw( deps: DepsMut, - timestamp: Timestamp, address: Addr, denom: String, ) -> Result { - let config = CONFIG.load(deps.storage)?; - let unbondings: Vec<(u64, Bond)> = UNBOND .prefix((&address, &denom)) .range(deps.storage, None, None, Order::Ascending) .take(MAX_PAGE_LIMIT as usize) .collect::>>()?; - let mut refund_amount = Uint128::zero(); - ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + let mut refund_amount = Uint128::zero(); for unbonding in unbondings { let (ts, bond) = unbonding; - if timestamp.minus_nanos(config.unbonding_period.u64()) >= bond.timestamp { + if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { let denom = bond.asset.denom; refund_amount = refund_amount.checked_add(bond.asset.amount)?; @@ -199,7 +205,7 @@ pub(crate) fn update_config( info: MessageInfo, epoch_manager_addr: Option, pool_manager_addr: Option, - unbonding_period: Option, + unbonding_period: Option, growth_rate: Option, ) -> Result { // check the owner is the one who sent the message @@ -249,7 +255,7 @@ pub fn claim(deps: DepsMut, info: MessageInfo) -> Result Result { @@ -449,7 +454,7 @@ pub(crate) fn on_epoch_created( // This happens only on the very first epoch where Global has not been initialised yet if global.is_none() { let initial_global_index = GlobalIndex { - timestamp: env.block.time, + last_updated: current_epoch.id, ..Default::default() }; GLOBAL.save(deps.storage, &initial_global_index)?; @@ -457,7 +462,7 @@ pub(crate) fn on_epoch_created( deps.storage, ¤t_epoch.id.to_be_bytes(), &Epoch { - id: current_epoch.id.into(), + id: current_epoch.id, start_time: current_epoch.start_time, global_index: initial_global_index, ..Epoch::default() @@ -523,10 +528,12 @@ pub(crate) fn on_epoch_created( // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in // the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - let next_epoch_id = Uint64::new(current_epoch.id).checked_add(Uint64::one())?; + let next_epoch_id = Uint64::new(current_epoch.id) + .checked_add(Uint64::one())? + .u64(); EPOCHS.save( deps.storage, - &next_epoch_id.u64().to_be_bytes(), + &next_epoch_id.to_be_bytes(), &Epoch { id: next_epoch_id, start_time: current_epoch.start_time.plus_days(1), diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 233a55ab..92dda2e8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -2,9 +2,9 @@ use cosmwasm_std::{ensure, entry_point, from_json, Addr, Coin, Order, Reply, Uin use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::{get_contract_version, set_contract_version}; use cw_utils::parse_reply_execute_data; -use white_whale_std::pool_network::asset; use white_whale_std::bonding_manager::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use white_whale_std::pool_network::asset; use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; @@ -65,15 +65,15 @@ pub fn execute( match msg { ExecuteMsg::Bond => { let asset_to_bond = helpers::validate_funds(&deps, &info)?; - commands::bond(deps, env.block.time, info, env, asset_to_bond) + commands::bond(deps, info, env, asset_to_bond) } ExecuteMsg::Unbond { asset } => { cw_utils::nonpayable(&info)?; - commands::unbond(deps, env.block.time, info, env, asset) + commands::unbond(deps, info, env, asset) } ExecuteMsg::Withdraw { denom } => { cw_utils::nonpayable(&info)?; - commands::withdraw(deps, env.block.time, info.sender, denom) + commands::withdraw(deps, info.sender, denom) } ExecuteMsg::UpdateConfig { epoch_manager_addr, @@ -94,7 +94,7 @@ pub fn execute( ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), ExecuteMsg::Claim => commands::claim(deps, info), ExecuteMsg::EpochChangedHook { current_epoch } => { - commands::on_epoch_created(deps, env, info, current_epoch) + commands::on_epoch_created(deps, info, current_epoch) } ExecuteMsg::UpdateOwnership(action) => { cw_utils::nonpayable(&info)?; @@ -104,7 +104,7 @@ pub fn execute( } #[entry_point] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::Config => Ok(to_json_binary(&queries::query_config(deps)?)?), QueryMsg::Bonded { address } => Ok(to_json_binary(&queries::query_bonded(deps, address)?)?), @@ -121,20 +121,29 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Ok(to_json_binary( - &queries::query_withdrawable(deps, env.block.time, address, denom)?, + &queries::query_withdrawable(deps, address, denom)?, )?), QueryMsg::Weight { address, - timestamp, + epoch_id, global_index, } => { - // If timestamp is not provided, use current block time - let timestamp = timestamp.unwrap_or(env.block.time); + let epoch_id = if let Some(epoch_id) = epoch_id { + epoch_id + } else { + // If epoch_id is not provided, use current epoch + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + current_epoch.epoch.id + }; - // TODO: Make better timestamp handling Ok(to_json_binary(&queries::query_weight( deps, - timestamp, + epoch_id, address, global_index, )?)?) diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 05eacdb2..0e0d8975 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,8 +1,9 @@ use std::collections::{HashSet, VecDeque}; -use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Uint128, Uint64}; use cw_storage_plus::Bound; +use crate::ContractError; use white_whale_std::bonding_manager::{ Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, UnbondingResponse, WithdrawableResponse, @@ -45,7 +46,7 @@ pub(crate) fn query_bonded(deps: Deps, address: Option) -> StdResult) -> Option> { /// unbonding period and can be withdrawn. pub(crate) fn query_withdrawable( deps: Deps, - timestamp: Timestamp, address: String, denom: String, ) -> StdResult { - let config = CONFIG.load(deps.storage)?; let unbonding: StdResult> = UNBOND .prefix((&deps.api.addr_validate(address.as_str())?, &denom)) .range(deps.storage, None, None, Order::Ascending) .take(MAX_PAGE_LIMIT as usize) .collect(); + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + let mut withdrawable_amount = Uint128::zero(); for (_, bond) in unbonding? { - if timestamp.minus_nanos(config.unbonding_period.u64()) >= bond.timestamp { + if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { withdrawable_amount = withdrawable_amount.checked_add(bond.asset.amount)?; } } @@ -142,7 +148,7 @@ pub(crate) fn query_withdrawable( /// Queries the current weight of the given address. pub(crate) fn query_weight( deps: Deps, - timestamp: Timestamp, + epoch_id: u64, address: String, global_index: Option, ) -> StdResult { @@ -163,11 +169,11 @@ pub(crate) fn query_weight( for (_, mut bond) in bonds? { bond.weight = get_weight( - timestamp, + epoch_id, bond.weight, bond.asset.amount, config.growth_rate, - bond.timestamp, + bond.updated_last, )?; if !unique_denoms.contains(&bond.asset.denom) { @@ -189,11 +195,11 @@ pub(crate) fn query_weight( }; global_index.weight = get_weight( - timestamp, + epoch_id, global_index.weight, global_index.bonded_amount, config.growth_rate, - global_index.timestamp, + global_index.last_updated, )?; // Represents the share of the global weight that the address has @@ -209,7 +215,7 @@ pub(crate) fn query_weight( weight: total_bond_weight, global_weight: global_index.weight, share, - timestamp, + epoch_id, }) } @@ -221,10 +227,12 @@ pub fn query_global_index(deps: Deps) -> StdResult { /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. -pub fn get_expiring_epoch(deps: Deps) -> StdResult> { +pub fn get_expiring_epoch(deps: Deps) -> Result, ContractError> { let config = CONFIG.load(deps.storage)?; // Adding 1 because we store the future epoch in the map also, so grace_period + 1 - let grace_period_plus_future_epoch = config.grace_period.u64() + 1u64; + let grace_period_plus_future_epoch = Uint64::new(config.grace_period) + .checked_add(Uint64::one())? + .u64(); // Take grace_period + 1 and then slice last one off let epochs = EPOCHS .range(deps.storage, None, None, Order::Descending) @@ -251,7 +259,9 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { pub fn get_claimable_epochs(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; // Adding 1 because we store the future epoch in the map also, so grace_period + 1 - let grace_period = config.grace_period.u64() + 1; + let grace_period = Uint64::new(config.grace_period) + .checked_add(Uint64::one())? + .u64(); // Take grace_period + 1 and then slice last one off let mut epochs = EPOCHS .range(deps.storage, None, None, Order::Descending) diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 6210e2fc..120ae7cb 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,7 +1,5 @@ use crate::ContractError; -use cosmwasm_std::{ - Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Timestamp, Uint128, Uint64, -}; +use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Uint128}; use cw_storage_plus::{Item, Map}; use white_whale_std::bonding_manager::{Bond, Config, Epoch, GlobalIndex}; @@ -12,27 +10,27 @@ pub const CONFIG: Item = Item::new("config"); pub const BOND: Map<(&Addr, &Denom), Bond> = Map::new("bond"); pub const UNBOND: Map<(&Addr, &Denom, u64), Bond> = Map::new("unbond"); pub const GLOBAL: Item = Item::new("global"); -pub const LAST_CLAIMED_EPOCH: Map<&Addr, Uint64> = Map::new("last_claimed_epoch"); +pub const LAST_CLAIMED_EPOCH: Map<&Addr, u64> = Map::new("last_claimed_epoch"); pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); /// Updates the local weight of the given address. pub fn update_local_weight( deps: &mut DepsMut, address: Addr, - timestamp: Timestamp, + current_epoch_id: u64, mut bond: Bond, ) -> Result { let config = CONFIG.load(deps.storage)?; bond.weight = get_weight( - timestamp, + current_epoch_id, bond.weight, bond.asset.amount, config.growth_rate, - bond.timestamp, + bond.updated_last, )?; - bond.timestamp = timestamp; + bond.updated_last = current_epoch_id; let denom: &String = &bond.asset.denom; @@ -45,20 +43,20 @@ pub fn update_local_weight( /// Updates the global weight of the contract. pub fn update_global_weight( deps: &mut DepsMut, - timestamp: Timestamp, + current_epoch_id: u64, mut global_index: GlobalIndex, ) -> Result { let config = CONFIG.load(deps.storage)?; global_index.weight = get_weight( - timestamp, + current_epoch_id, global_index.weight, global_index.bonded_amount, config.growth_rate, - global_index.timestamp, + global_index.last_updated, )?; - global_index.timestamp = timestamp; + global_index.last_updated = current_epoch_id; //todo remove? done outside of this function. Or remove outside GLOBAL.save(deps.storage, &global_index)?; @@ -66,21 +64,20 @@ pub fn update_global_weight( Ok(global_index) } -/// Calculates the bonding weight of the given amount for the provided timestamps. +/// Calculates the bonding weight of the given amount for the provided epochs. pub fn get_weight( - current_timestamp: Timestamp, + current_epoch_id: u64, weight: Uint128, amount: Uint128, growth_rate: Decimal, - timestamp: Timestamp, + epoch_id: u64, ) -> StdResult { - let time_factor = if timestamp == Timestamp::default() { + let time_factor = if current_epoch_id == epoch_id { Uint128::zero() } else { Uint128::from( - current_timestamp - .seconds() - .checked_sub(timestamp.seconds()) + current_epoch_id + .checked_sub(epoch_id) .ok_or_else(|| StdError::generic_err("Error calculating time_factor"))?, ) }; @@ -96,7 +93,7 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { // last epochs within the grace period let epochs = EPOCHS .range(deps.storage, None, None, Order::Descending) - .take(grace_period.u64() as usize) + .take(grace_period as usize) .map(|item| { let (_, epoch) = item?; Ok(epoch) @@ -105,7 +102,7 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { // if the epochs vector's length is the same as the grace period it means there is one epoch that // is expiring once the new one is created i.e. the last epoch in the vector - if epochs.len() == grace_period.u64() as usize { + if epochs.len() == grace_period as usize { Ok(Some(epochs.last().cloned().unwrap_or_default())) } else { // nothing is expiring yet diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index 14895e28..d4b6de33 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -72,7 +72,7 @@ fn test_bond_successfully() { weight: Uint128::new(1_000u128), global_weight: Uint128::new(1_000u128), share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571883819879305533u64), + epoch_id: Timestamp::from_nanos(1571883819879305533u64), }, ); @@ -137,7 +137,7 @@ fn test_bond_successfully() { weight: Uint128::new(5_000u128), global_weight: Uint128::new(950_409_000u128), share: Decimal::from_ratio(5_000u128, 950_409_000u128), - timestamp: Timestamp::from_nanos(1572013419879305533u64), + epoch_id: Timestamp::from_nanos(1572013419879305533u64), }, ) .query_bonded(None, |res| { @@ -170,7 +170,7 @@ fn test_bond_successfully() { weight: Uint128::new(734_404_000u128), global_weight: Uint128::new(1_728_009_000u128), share: Decimal::from_ratio(734_404_000u128, 1_728_009_000u128), - timestamp: Timestamp::from_nanos(1572099819879305533u64), + epoch_id: Timestamp::from_nanos(1572099819879305533u64), }, ) .assert_bonding_weight_response( @@ -180,7 +180,7 @@ fn test_bond_successfully() { weight: Uint128::new(432_005_000u128), global_weight: Uint128::new(1_728_009_000u128), share: Decimal::from_ratio(432_005_000u128, 1_728_009_000u128), - timestamp: Timestamp::from_nanos(1572099819879305533u64), + epoch_id: Timestamp::from_nanos(1572099819879305533u64), }, ) .query_bonded(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 6ea88c28..3eb472bd 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -456,7 +456,7 @@ impl TestingSuite { &self.bonding_manager_addr, &QueryMsg::Weight { address, - timestamp: Some(self.app.block_info().time), + epoch_id: Some(self.app.block_info().time), global_index: None, }, ) diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 355667aa..953a2404 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -2,7 +2,7 @@ use crate::epoch_manager::epoch_manager::Epoch as EpochV2; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ - to_json_binary, Addr, Coin, CosmosMsg, Decimal, StdResult, Timestamp, Uint128, Uint64, WasmMsg, + to_json_binary, Addr, Coin, CosmosMsg, Decimal, StdResult, Timestamp, Uint128, WasmMsg, }; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; @@ -16,21 +16,22 @@ pub struct Config { pub distribution_denom: String, /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can /// be withdrawn - pub unbonding_period: Uint64, + pub unbonding_period: u64, /// A fraction that controls the effect of time on the weight of a bond. If the growth rate is set /// to zero, time will have no impact on the weight. pub growth_rate: Decimal, /// Denom of the asset to be bonded. Can't only be set at instantiation. pub bonding_assets: Vec, - /// The duration of the grace period in epochs, i.e. how many expired epochs can be claimed - pub grace_period: Uint64, + /// Grace period the maximum age of a epoch bucket before it's considered expired and fees + /// are forwarded from it + pub grace_period: u64, } #[cw_serde] #[derive(Default)] pub struct Epoch { // Epoch identifier - pub id: Uint64, + pub id: u64, // Epoch start time pub start_time: Timestamp, // Initial fees to be distributed in this epoch. @@ -48,9 +49,9 @@ pub struct Bond { /// The amount of bonded tokens. pub asset: Coin, /// The epoch id at which the Bond was created. - pub created_at_epoch: Uint64, - /// The timestamp at which the bond was done. - pub timestamp: Timestamp, + pub created_at_epoch: u64, + /// The epoch id at which the bond was last time updated. + pub updated_last: u64, /// The weight of the bond at the given block height. pub weight: Uint128, } @@ -63,7 +64,7 @@ impl Default for Bond { amount: Uint128::zero(), }, created_at_epoch: Default::default(), - timestamp: Timestamp::default(), + updated_last: Default::default(), weight: Uint128::zero(), } } @@ -76,8 +77,8 @@ pub struct GlobalIndex { pub bonded_amount: Uint128, /// Assets that are bonded in the contract. pub bonded_assets: Vec, - /// The timestamp at which the total bond was registered. - pub timestamp: Timestamp, + /// The epoch id at which the total bond was updated. + pub last_updated: u64, /// The total weight of the bond at the given block height. pub weight: Uint128, } @@ -86,15 +87,16 @@ pub struct GlobalIndex { pub struct InstantiateMsg { /// Denom to be swapped to and rewarded pub distribution_denom: String, - /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can + /// Unbonding period in epochs. The time (in epochs) that needs to pass before an unbonded position can /// be withdrawn - pub unbonding_period: Uint64, + pub unbonding_period: u64, /// Weight grow rate. Needs to be between 0 and 1. pub growth_rate: Decimal, /// [String] denoms of the assets that can be bonded. pub bonding_assets: Vec, - /// Grace period the maximum age of a bucket before fees are forwarded from it - pub grace_period: Uint64, + /// Grace period the maximum age of a epoch bucket before it's considered expired and fees + /// are forwarded from it + pub grace_period: u64, /// The epoch manager contract pub epoch_manager_addr: String, } @@ -126,7 +128,7 @@ pub enum ExecuteMsg { /// The new pool manager address. pool_manager_addr: Option, /// The unbonding period. - unbonding_period: Option, + unbonding_period: Option, /// The new growth rate. growth_rate: Option, }, @@ -190,7 +192,7 @@ pub enum QueryMsg { /// The address to check for weight. address: String, /// The timestamp to check for weight. If none is provided, the current block time is used. - timestamp: Option, + epoch_id: Option, /// The global index to check for weight. If none is provided, the current global index is used. global_index: Option, }, @@ -221,7 +223,7 @@ pub struct BondedResponse { pub bonded_assets: Vec, /// If Some, the epoch id at which the user/address bonded first time. None is used when this /// Response is used to check the bonded assets in the contract. - pub first_bonded_epoch_id: Option, + pub first_bonded_epoch_id: Option, } /// Response for the Unbonding query @@ -251,8 +253,8 @@ pub struct BondingWeightResponse { pub global_weight: Uint128, /// The share the address has of the rewards at the particular timestamp. pub share: Decimal, - /// The timestamp at which the weight was calculated. - pub timestamp: Timestamp, + /// The epoch id at which the weight was calculated. + pub epoch_id: u64, } /// Creates a message to fill rewards on the whale lair contract. From eaaaa4fa5f933dc8cd6f2ee3c48973c3b99f6392 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 9 May 2024 13:23:08 +0100 Subject: [PATCH 25/51] refactor: rename bonding-manager.epochs to reward buckets --- .../schema/bonding-manager.json | 106 ++++----- .../bonding-manager/schema/raw/execute.json | 2 +- .../schema/raw/instantiate.json | 2 +- .../bonding-manager/schema/raw/query.json | 4 +- .../schema/raw/response_to_claimable.json | 96 ++++---- .../schema/raw/response_to_config.json | 2 +- .../bonding-manager/src/commands.rs | 205 +++++++++--------- .../bonding-manager/src/contract.rs | 32 +-- .../bonding-manager/src/error.rs | 2 +- .../bonding-manager/src/helpers.rs | 96 +------- .../bonding-manager/src/queries.rs | 100 +++++---- .../bonding-manager/src/state.rs | 18 +- .../bonding-manager/src/tests/suite.rs | 14 +- .../white-whale-std/src/bonding_manager.rs | 44 ++-- 14 files changed, 322 insertions(+), 401 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index ee01742b..bdc8c4e2 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -31,7 +31,7 @@ "type": "string" }, "grace_period": { - "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "description": "Grace period the maximum age of a reward bucket before it's considered expired and fees are forwarded from it", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -185,7 +185,7 @@ ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Epoch Changed hook implementation. Creates a new reward bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring reward bucket (only 21 of them are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" @@ -538,7 +538,7 @@ ] }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Returns the [RewardBucket]s that can be claimed by an address.", "type": "object", "required": [ "claimable" @@ -548,7 +548,7 @@ "type": "object", "properties": { "address": { - "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "description": "The address to check for claimable reward buckets. If none is provided, all possible reward buckets stored in the contract that can potentially be claimed are returned.", "type": [ "string", "null" @@ -706,17 +706,17 @@ }, "claimable": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ClaimableEpochsResponse", + "title": "ClaimableRewardBucketsResponse", "type": "object", "required": [ - "epochs" + "reward_buckets" ], "properties": { - "epochs": { - "description": "The epochs that can be claimed by the address.", + "reward_buckets": { + "description": "The reward buckets that can be claimed by the address.", "type": "array", "items": { - "$ref": "#/definitions/Epoch" + "$ref": "#/definitions/RewardBucket" } } }, @@ -737,49 +737,6 @@ } } }, - "Epoch": { - "type": "object", - "required": [ - "available", - "claimed", - "global_index", - "id", - "start_time", - "total" - ], - "properties": { - "available": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "claimed": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "global_index": { - "$ref": "#/definitions/GlobalIndex" - }, - "id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "start_time": { - "$ref": "#/definitions/Timestamp" - }, - "total": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - }, "GlobalIndex": { "type": "object", "required": [ @@ -821,6 +778,49 @@ }, "additionalProperties": false }, + "RewardBucket": { + "type": "object", + "required": [ + "available", + "claimed", + "epoch_start_time", + "global_index", + "id", + "total" + ], + "properties": { + "available": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "claimed": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "epoch_start_time": { + "$ref": "#/definitions/Timestamp" + }, + "global_index": { + "$ref": "#/definitions/GlobalIndex" + }, + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -873,7 +873,7 @@ ] }, "grace_period": { - "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "description": "Grace period the maximum age of a reward bucket before it's considered expired and fees are forwarded from it", "type": "integer", "format": "uint64", "minimum": 0.0 diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index c6a562de..4df3f63b 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -124,7 +124,7 @@ ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Epoch Changed hook implementation. Creates a new reward bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring reward bucket (only 21 of them are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json index f93d08e6..e331cda5 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json @@ -27,7 +27,7 @@ "type": "string" }, "grace_period": { - "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "description": "Grace period the maximum age of a reward bucket before it's considered expired and fees are forwarded from it", "type": "integer", "format": "uint64", "minimum": 0.0 diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index 3c5ab6ae..7139a19f 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -157,7 +157,7 @@ ] }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Returns the [RewardBucket]s that can be claimed by an address.", "type": "object", "required": [ "claimable" @@ -167,7 +167,7 @@ "type": "object", "properties": { "address": { - "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "description": "The address to check for claimable reward buckets. If none is provided, all possible reward buckets stored in the contract that can potentially be claimed are returned.", "type": [ "string", "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 41023177..e05fe961 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 @@ -1,16 +1,16 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ClaimableEpochsResponse", + "title": "ClaimableRewardBucketsResponse", "type": "object", "required": [ - "epochs" + "reward_buckets" ], "properties": { - "epochs": { - "description": "The epochs that can be claimed by the address.", + "reward_buckets": { + "description": "The reward buckets that can be claimed by the address.", "type": "array", "items": { - "$ref": "#/definitions/Epoch" + "$ref": "#/definitions/RewardBucket" } } }, @@ -31,49 +31,6 @@ } } }, - "Epoch": { - "type": "object", - "required": [ - "available", - "claimed", - "global_index", - "id", - "start_time", - "total" - ], - "properties": { - "available": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "claimed": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "global_index": { - "$ref": "#/definitions/GlobalIndex" - }, - "id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "start_time": { - "$ref": "#/definitions/Timestamp" - }, - "total": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - }, "GlobalIndex": { "type": "object", "required": [ @@ -115,6 +72,49 @@ }, "additionalProperties": false }, + "RewardBucket": { + "type": "object", + "required": [ + "available", + "claimed", + "epoch_start_time", + "global_index", + "id", + "total" + ], + "properties": { + "available": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "claimed": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "epoch_start_time": { + "$ref": "#/definitions/Timestamp" + }, + "global_index": { + "$ref": "#/definitions/GlobalIndex" + }, + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json index 20a56f53..498ca242 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json @@ -32,7 +32,7 @@ ] }, "grace_period": { - "description": "Grace period the maximum age of a epoch bucket before it's considered expired and fees are forwarded from it", + "description": "Grace period the maximum age of a reward bucket before it's considered expired and fees are forwarded from it", "type": "integer", "format": "uint64", "minimum": 0.0 diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 63441446..697ab5a1 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -3,14 +3,15 @@ use cosmwasm_std::{ StdError, StdResult, SubMsg, Uint128, Uint64, }; -use white_whale_std::bonding_manager::{Bond, Epoch, GlobalIndex}; +use white_whale_std::bonding_manager::{Bond, GlobalIndex, RewardBucket}; +use white_whale_std::epoch_manager::epoch_manager::Epoch; use white_whale_std::pool_network::asset; use crate::helpers::validate_growth_rate; -use crate::queries::{get_expiring_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; +use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight, MAX_PAGE_LIMIT}; use crate::state::{ - update_global_weight, update_local_weight, BOND, CONFIG, EPOCHS, GLOBAL, LAST_CLAIMED_EPOCH, - UNBOND, + update_global_weight, update_local_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, + REWARD_BUCKETS, UNBOND, }; use crate::{helpers, ContractError}; @@ -22,7 +23,7 @@ pub(crate) fn bond( asset: Coin, ) -> Result { println!("bonding"); - helpers::validate_epochs(&deps)?; + helpers::validate_buckets(&deps)?; helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps, &env)?; println!("bonding 2"); @@ -49,8 +50,7 @@ pub(crate) fn bond( // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; - bond = update_local_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; - BOND.save(deps.storage, (&info.sender, &asset.denom), &bond)?; + update_local_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); @@ -59,9 +59,7 @@ pub(crate) fn bond( global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; global_index.bonded_assets = asset::aggregate_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; - - GLOBAL.save(deps.storage, &global_index)?; + update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; Ok(Response::default().add_attributes(vec![ ("action", "bond".to_string()), @@ -243,24 +241,24 @@ pub(crate) fn update_config( /// Claims pending rewards for the sender. pub fn claim(deps: DepsMut, info: MessageInfo) -> Result { - let claimable_epochs_for_user = - query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.epochs; + let claimable_reward_buckets_for_user = + query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.reward_buckets; ensure!( - !claimable_epochs_for_user.is_empty(), + !claimable_reward_buckets_for_user.is_empty(), ContractError::NothingToClaim ); - let mut claimable_fees = vec![]; + let mut claimable_rewards = vec![]; let mut attributes = vec![]; - for mut epoch in claimable_epochs_for_user.clone() { + for mut reward_bucket in claimable_reward_buckets_for_user.clone() { let bonding_weight_response_for_epoch = query_weight( deps.as_ref(), - epoch.id, + reward_bucket.id, info.sender.to_string(), - Some(epoch.global_index.clone()), + Some(reward_bucket.global_index.clone()), )?; - // if the user has no share in the epoch, skip it + // if the user has no share in the bucket, skip it if bonding_weight_response_for_epoch.share.is_zero() { continue; }; @@ -271,20 +269,20 @@ pub fn claim(deps: DepsMut, info: MessageInfo) -> Result = epoch + let reward_validation: Result<(), StdError> = reward_bucket .available .iter() - .find(|available_fee| available_fee.denom == fee.denom) + .find(|available_fee| available_fee.denom == reward.denom) .map(|available_fee| { - if reward > available_fee.amount { + if user_reward > available_fee.amount { attributes.push(( "error", ContractError::InvalidReward { - reward, + reward: user_reward, available: available_fee.amount, } .to_string(), @@ -294,45 +292,45 @@ pub fn claim(deps: DepsMut, info: MessageInfo) -> Result {} Err(_) => continue, } - let denom = &fee.denom; - // add the reward to the claimable fees - claimable_fees = asset::aggregate_coins( - claimable_fees, + let denom = &reward.denom; + // add the reward + claimable_rewards = asset::aggregate_coins( + claimable_rewards, vec![Coin { denom: denom.to_string(), - amount: reward, + amount: user_reward, }], )?; - // modify the epoch to reflect the new available and claimed amount - for available_fee in epoch.available.iter_mut() { - if available_fee.denom == fee.denom { - available_fee.amount = available_fee.amount.saturating_sub(reward); + // modify the bucket to reflect the new available and claimed amount + for available_fee in reward_bucket.available.iter_mut() { + if available_fee.denom == reward.denom { + available_fee.amount = available_fee.amount.saturating_sub(user_reward); } } - if epoch.claimed.is_empty() { - epoch.claimed = vec![Coin { + if reward_bucket.claimed.is_empty() { + reward_bucket.claimed = vec![Coin { denom: denom.to_string(), - amount: reward, + amount: user_reward, }]; } else { - for claimed_fee in epoch.claimed.iter_mut() { - if claimed_fee.denom == fee.denom { - claimed_fee.amount = claimed_fee.amount.checked_add(reward)?; + for claimed_reward in reward_bucket.claimed.iter_mut() { + if claimed_reward.denom == reward.denom { + claimed_reward.amount = claimed_reward.amount.checked_add(user_reward)?; } // sanity check, should never happen - for total_fee in epoch.total.iter() { - if total_fee.denom == claimed_fee.denom { + for total_reward in reward_bucket.total.iter() { + if total_reward.denom == claimed_reward.denom { ensure!( - claimed_fee.amount <= total_fee.amount, + claimed_reward.amount <= total_reward.amount, ContractError::InvalidShare ); } @@ -340,20 +338,24 @@ pub fn claim(deps: DepsMut, info: MessageInfo) -> Result Result { println!( "EPOCHS: {:?}", - EPOCHS + REWARD_BUCKETS .keys(deps.storage, None, None, Order::Descending) .collect::>() ); - // Finding the most recent EpochID - let upcoming_epoch_id = match EPOCHS + // Finding the most recent bucket + let upcoming_bucket_id = match REWARD_BUCKETS .keys(deps.storage, None, None, Order::Descending) .next() { - Some(epoch_id) => epoch_id?, + Some(bucket_id) => bucket_id?, None => return Err(ContractError::Unauthorized), }; @@ -422,7 +424,7 @@ pub(crate) fn fill_rewards( // of all the LP token withdrawals and swaps // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned // If this became an issue would could look at replys instead of the query - EPOCHS.update(deps.storage, &upcoming_epoch_id, |bucket| -> StdResult<_> { + REWARD_BUCKETS.update(deps.storage, upcoming_bucket_id, |bucket| -> StdResult<_> { let mut bucket = bucket.unwrap_or_default(); bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; @@ -437,13 +439,12 @@ pub(crate) fn fill_rewards( pub(crate) fn on_epoch_created( deps: DepsMut, info: MessageInfo, - current_epoch: white_whale_std::epoch_manager::epoch_manager::Epoch, + current_epoch: Epoch, ) -> Result { cw_utils::nonpayable(&info)?; println!("EpochChangedHook: {:?}", current_epoch); - // A new epoch has been created, update rewards bucket and forward the expiring epoch - // Store epoch and verify the sender is the epoch manager + // A new epoch has been created, update rewards bucket and forward the expiring bucket let config = CONFIG.load(deps.storage)?; ensure!( info.sender == config.epoch_manager_addr, @@ -458,89 +459,81 @@ pub(crate) fn on_epoch_created( ..Default::default() }; GLOBAL.save(deps.storage, &initial_global_index)?; - EPOCHS.save( + REWARD_BUCKETS.save( deps.storage, - ¤t_epoch.id.to_be_bytes(), - &Epoch { + current_epoch.id, + &RewardBucket { id: current_epoch.id, - start_time: current_epoch.start_time, + epoch_start_time: current_epoch.start_time, global_index: initial_global_index, - ..Epoch::default() + ..RewardBucket::default() }, )?; } let global = GLOBAL.load(deps.storage)?; - // update the global index for the current epoch, take the current snapshot of the global index - EPOCHS.update( + // update the global index for the current bucket, take the current snapshot of the global index + REWARD_BUCKETS.update( deps.storage, - ¤t_epoch.id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.global_index = global; - Ok(epoch) + current_epoch.id, + |reward_bucket| -> StdResult<_> { + let mut reward_bucket = reward_bucket.unwrap_or_default(); + reward_bucket.global_index = global; + Ok(reward_bucket) }, )?; // todo to delete once the testing is done - let all_epochs: Vec = EPOCHS + let all_buckets: Vec = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) .map(|item| { - let (_, epoch) = item?; - Ok(epoch) + let (_, bucket) = item?; + Ok(bucket) }) - .collect::>>()?; + .collect::>>()?; - println!("EPOCHS: {:?}", all_epochs); + println!("EPOCHS: {:?}", all_buckets); - // forward fees from the expiring epoch to the new one. - let mut expiring_epoch = get_expiring_epoch(deps.as_ref())?; - if let Some(expiring_epoch) = expiring_epoch.as_mut() { - // Load all the available assets from the expiring epoch - let amount_to_be_forwarded = EPOCHS - .load(deps.storage, &expiring_epoch.id.to_be_bytes())? + // forward fees from the expiring bucket to the new one. + let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; + if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { + // Load all the available assets from the expiring bucket + let amount_to_be_forwarded = REWARD_BUCKETS + .load(deps.storage, expiring_bucket.id)? .available; - EPOCHS.update( - deps.storage, - ¤t_epoch.id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = - asset::aggregate_coins(epoch.available, amount_to_be_forwarded.clone())?; - epoch.total = asset::aggregate_coins(epoch.total, amount_to_be_forwarded)?; - - Ok(epoch) - }, - )?; - // Set the available assets for the expiring epoch to an empty vec now that they have been + REWARD_BUCKETS.update(deps.storage, current_epoch.id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = + asset::aggregate_coins(bucket.available, amount_to_be_forwarded.clone())?; + bucket.total = asset::aggregate_coins(bucket.total, amount_to_be_forwarded)?; + + Ok(bucket) + })?; + // Set the available assets for the expiring bucket to an empty vec now that they have been // forwarded - EPOCHS.update( - deps.storage, - &expiring_epoch.id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = vec![]; - Ok(epoch) - }, - )?; + REWARD_BUCKETS.update(deps.storage, expiring_bucket.id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = vec![]; + Ok(bucket) + })?; } // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in - // the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + // the next epoch. Also, forwards the expiring bucket (only 21 bucket are live at a given moment) let next_epoch_id = Uint64::new(current_epoch.id) .checked_add(Uint64::one())? .u64(); - EPOCHS.save( + REWARD_BUCKETS.save( deps.storage, - &next_epoch_id.to_be_bytes(), - &Epoch { + next_epoch_id, + &RewardBucket { id: next_epoch_id, - start_time: current_epoch.start_time.plus_days(1), + epoch_start_time: current_epoch.start_time.plus_days(1), // this global index is to be updated the next time this hook is called, as this future epoch // will become the current one global_index: Default::default(), - ..Epoch::default() + ..RewardBucket::default() }, )?; diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 92dda2e8..a903f65c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -8,7 +8,7 @@ use white_whale_std::pool_network::asset; use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; -use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, EPOCHS}; +use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, REWARD_BUCKETS}; use crate::{commands, queries}; // version info for migration info @@ -197,7 +197,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result return Err(ContractError::Unauthorized), }; - EPOCHS.update(deps.storage, &upcoming_epoch_id, |epoch| -> StdResult<_> { - let mut upcoming_epoch = epoch.unwrap_or_default(); - upcoming_epoch.available = asset::aggregate_coins( - upcoming_epoch.available, - vec![to_be_distribution_asset.clone()], - )?; - upcoming_epoch.total = asset::aggregate_coins( - upcoming_epoch.total, - vec![to_be_distribution_asset.clone()], - )?; - Ok(upcoming_epoch) - })?; + REWARD_BUCKETS.update( + deps.storage, + upcoming_epoch_id, + |epoch| -> StdResult<_> { + let mut upcoming_epoch = epoch.unwrap_or_default(); + upcoming_epoch.available = asset::aggregate_coins( + upcoming_epoch.available, + vec![to_be_distribution_asset.clone()], + )?; + upcoming_epoch.total = asset::aggregate_coins( + upcoming_epoch.total, + vec![to_be_distribution_asset.clone()], + )?; + Ok(upcoming_epoch) + }, + )?; } Ok(Response::new() diff --git a/contracts/liquidity_hub/bonding-manager/src/error.rs b/contracts/liquidity_hub/bonding-manager/src/error.rs index 9a44ea25..a3be149b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/error.rs +++ b/contracts/liquidity_hub/bonding-manager/src/error.rs @@ -69,7 +69,7 @@ pub enum ContractError { InvalidShare, #[error( - "Invalid reward amount. Reward: {reward}, but only {available} available in the epoch." + "Invalid reward amount. Reward: {reward}, but only {available} available in the reward bucket." )] InvalidReward { reward: Uint128, available: Uint128 }, } diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 67cbe51c..aeac395b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,11 +1,11 @@ use cosmwasm_std::{ ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, ReplyOn, - StdResult, SubMsg, Timestamp, Uint64, WasmMsg, + StdResult, SubMsg, WasmMsg, }; use cw_utils::PaymentError; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Config}; +use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, Config}; use white_whale_std::constants::{DAY_IN_SECONDS, LP_SYMBOL}; -use white_whale_std::epoch_manager::epoch_manager::{EpochConfig, EpochResponse}; +use white_whale_std::epoch_manager::epoch_manager::EpochResponse; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, }; @@ -13,7 +13,7 @@ use white_whale_std::pool_manager::{ use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; use crate::queries::query_claimable; -use crate::state::{CONFIG, EPOCHS}; +use crate::state::{CONFIG, REWARD_BUCKETS}; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { @@ -55,11 +55,11 @@ pub fn validate_funds(deps: &DepsMut, info: &MessageInfo) -> Result Result<(), ContractError> { // Do a smart query for Claimable - let claimable_rewards: ClaimableEpochsResponse = + let claimable_rewards: ClaimableRewardBucketsResponse = query_claimable(deps.as_ref(), Some(info.sender.to_string())).unwrap(); // ensure the user has nothing to claim ensure!( - claimable_rewards.epochs.is_empty(), + claimable_rewards.reward_buckets.is_empty(), ContractError::UnclaimedRewards ); @@ -99,29 +99,6 @@ pub fn validate_bonding_for_current_epoch(deps: &DepsMut, env: &Env) -> Result<( Ok(()) } -//todo remove -/// Calculates the epoch id for any given timestamp based on the genesis epoch configuration. -pub fn calculate_epoch( - genesis_epoch_config: EpochConfig, - timestamp: Timestamp, -) -> StdResult { - let epoch_duration: Uint64 = genesis_epoch_config.duration; - - // if this is true, it means the epoch is before the genesis epoch. In that case return Epoch 0. - if Uint64::new(timestamp.nanos()) < genesis_epoch_config.genesis_epoch { - return Ok(Uint64::zero()); - } - - let elapsed_time = - Uint64::new(timestamp.nanos()).checked_sub(genesis_epoch_config.genesis_epoch)?; - - let epoch = elapsed_time - .checked_div(epoch_duration)? - .checked_add(Uint64::one())?; - - Ok(epoch) -} - // Used in FillRewards to search the funds for LP tokens and withdraw them // If we do get some LP tokens to withdraw they could be swapped to whale in the reply pub fn handle_lp_tokens( @@ -285,67 +262,14 @@ pub fn swap_coins_to_main_token( Ok(()) } -/// Validates that there are epochs in the state. If there are none, it means the system has just +/// Validates that there are reward buckets in the state. If there are none, it means the system has just /// been started and the epoch manager has still not created any epochs yet. -pub(crate) fn validate_epochs(deps: &DepsMut) -> Result<(), ContractError> { - let epochs = EPOCHS +pub(crate) fn validate_buckets(deps: &DepsMut) -> Result<(), ContractError> { + let reward_buckets = REWARD_BUCKETS .keys(deps.storage, None, None, Order::Descending) .collect::>>()?; - ensure!(!epochs.is_empty(), ContractError::Unauthorized); + ensure!(!reward_buckets.is_empty(), ContractError::Unauthorized); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_calculate_epoch() { - let genesis_epoch = EpochConfig { - duration: Uint64::from(86400000000000u64), // 1 day in nanoseconds - genesis_epoch: Uint64::from(1683212400000000000u64), // May 4th 2023 15:00:00 - }; - - // First bond timestamp equals genesis epoch - let first_bond_timestamp = Timestamp::from_nanos(1683212400000000000u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(1u64)); - - // First bond timestamp is one day after genesis epoch - let first_bond_timestamp = Timestamp::from_nanos(1683309600000000000u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(2u64)); - - // First bond timestamp is three days after genesis epoch - let first_bond_timestamp = Timestamp::from_nanos(1683471600000000000u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(4u64)); - - // First bond timestamp is before genesis epoch - let first_bond_timestamp = Timestamp::from_nanos(1683212300000000000u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::zero()); - - // First bond timestamp is within the same epoch as genesis epoch - let first_bond_timestamp = Timestamp::from_nanos(1683223200000000000u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(1u64)); - - // First bond timestamp is at the end of the genesis epoch, but not exactly (so it's still not epoch 2) - let first_bond_timestamp = Timestamp::from_nanos(1683298799999999999u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(1u64)); - - // First bond timestamp is exactly one nanosecond after the end of an epoch - let first_bond_timestamp = Timestamp::from_nanos(1683298800000000001u64); - let epoch = calculate_epoch(genesis_epoch.clone(), first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(2u64)); - - // First bond timestamp is June 13th 2023 10:56:53 - let first_bond_timestamp = Timestamp::from_nanos(1686653813000000000u64); - let epoch = calculate_epoch(genesis_epoch, first_bond_timestamp).unwrap(); - assert_eq!(epoch, Uint64::from(40u64)); - } -} diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 0e0d8975..73420d76 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -8,10 +8,11 @@ use white_whale_std::bonding_manager::{ Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, UnbondingResponse, WithdrawableResponse, }; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; +use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use crate::state::{ - get_weight, BOND, BONDING_ASSETS_LIMIT, CONFIG, EPOCHS, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, + get_weight, BOND, BONDING_ASSETS_LIMIT, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, + UNBOND, }; /// Queries the current configuration of the contract. @@ -225,71 +226,74 @@ pub fn query_global_index(deps: Deps) -> StdResult { Ok(global_index) } -/// Returns the epoch that is falling out the grace period, which is the one expiring after creating -/// a new epoch is created. -pub fn get_expiring_epoch(deps: Deps) -> Result, ContractError> { +/// Returns the reward bucket that is falling out the grace period, which is the one expiring +/// after creating a new epoch is created. +pub fn get_expiring_reward_bucket(deps: Deps) -> Result, ContractError> { let config = CONFIG.load(deps.storage)?; - // Adding 1 because we store the future epoch in the map also, so grace_period + 1 - let grace_period_plus_future_epoch = Uint64::new(config.grace_period) + // Adding 1 because we store the future bucket in the map also, so grace_period + 1 + let grace_period_plus_future_bucket = Uint64::new(config.grace_period) .checked_add(Uint64::one())? .u64(); // Take grace_period + 1 and then slice last one off - let epochs = EPOCHS + let buckets = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) - .take(grace_period_plus_future_epoch as usize) + .take(grace_period_plus_future_bucket as usize) .map(|item| { - let (_, epoch) = item?; - Ok(epoch) + let (_, bucket) = item?; + Ok(bucket) }) - .collect::>>()?; + .collect::>>()?; - // if the epochs vector's length is the same as the grace period it means there is one epoch that - // is expiring once the new one is created i.e. the last epoch in the vector - if epochs.len() == grace_period_plus_future_epoch as usize { - let expiring_epoch: Epoch = epochs.into_iter().last().unwrap_or_default(); - Ok(Some(expiring_epoch)) + // if the buckets vector's length is the same as the grace period it means there is one bucket that + // is expiring once the new one is created i.e. the last bucket in the vector + if buckets.len() == grace_period_plus_future_bucket as usize { + let expiring_reward_bucket: RewardBucket = buckets.into_iter().last().unwrap_or_default(); + Ok(Some(expiring_reward_bucket)) } else { // nothing is expiring yet Ok(None) } } -/// Returns the epochs that are within the grace period, i.e. the ones which fees can still be claimed. -/// The result is ordered by epoch id, descending. Thus, the first element is the current epoch. -pub fn get_claimable_epochs(deps: Deps) -> StdResult { +/// Returns the buckets that are within the grace period, i.e. the ones which fees can still be claimed. +/// The result is ordered by bucket id, descending. Thus, the first element is the current bucket. +pub fn get_claimable_reward_buckets(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - // Adding 1 because we store the future epoch in the map also, so grace_period + 1 + // Adding 1 because we store the future reward bucket in the map also, so grace_period + 1 let grace_period = Uint64::new(config.grace_period) .checked_add(Uint64::one())? .u64(); // Take grace_period + 1 and then slice last one off - let mut epochs = EPOCHS + let mut reward_buckets = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) .take(grace_period as usize) .map(|item| { - let (_, epoch) = item?; + let (_, bucket) = item?; - Ok(epoch) + Ok(bucket) }) - .collect::>>()?; + .collect::>>()?; - println!("epochs: {:?}", epochs); + println!("reward_buckets: {:?}", reward_buckets); - // Remove the upcoming epoch from stack - epochs.pop_front(); - epochs.retain(|epoch| !epoch.available.is_empty()); + // Remove the upcoming bucket from stack + reward_buckets.pop_front(); + reward_buckets.retain(|bucket| !bucket.available.is_empty()); - println!("epochs: {:?}", epochs); + println!("reward_buckets: {:?}", reward_buckets); - Ok(ClaimableEpochsResponse { - epochs: epochs.into(), + Ok(ClaimableRewardBucketsResponse { + reward_buckets: reward_buckets.into(), }) } -/// Returns the epochs that can be claimed by the given address. If no address is provided, -/// returns all possible epochs stored in the contract that can potentially be claimed. -pub fn query_claimable(deps: Deps, address: Option) -> StdResult { - let mut claimable_epochs = get_claimable_epochs(deps)?.epochs; +/// Returns the reward buckets that can be claimed by the given address. If no address is provided, +/// returns all possible buckets stored in the contract that can potentially be claimed. +pub fn query_claimable( + deps: Deps, + address: Option, +) -> StdResult { + let mut claimable_reward_buckets = get_claimable_reward_buckets(deps)?.reward_buckets; // if an address is provided, filter what's claimable for that address if let Some(address) = address { @@ -297,9 +301,9 @@ pub fn query_claimable(deps: Deps, address: Option) -> StdResult last_claimed_epoch); + claimable_reward_buckets.retain(|bucket| bucket.id > last_claimed_epoch); } else { // if the user doesn't have any last_claimed_epoch two things might be happening: // 1- the user has never bonded before @@ -309,29 +313,29 @@ pub fn query_claimable(deps: Deps, address: Option) -> StdResult { - // keep all epochs that are newer than the first bonded epoch - claimable_epochs.retain(|epoch| epoch.id > first_bonded_epoch_id); + // keep all buckets that are newer than the first bonded epoch + claimable_reward_buckets.retain(|bucket| bucket.id > first_bonded_epoch_id); } None => { // for sanity, it should never happen - claimable_epochs.clear(); + claimable_reward_buckets.clear(); } } } }; - // filter out epochs that have no available fees. This would only happen in case the grace period - // gets increased after epochs have expired, which would lead to make them available for claiming - // again without any available rewards, as those were forwarded to newer epochs. - claimable_epochs.retain(|epoch| !epoch.available.is_empty()); + // filter out buckets that have no available fees. This would only happen in case the grace period + // gets increased after buckets have expired, which would lead to make them available for claiming + // again without any available rewards, as those were forwarded to newer buckets. + claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } - Ok(ClaimableEpochsResponse { - epochs: claimable_epochs, + Ok(ClaimableRewardBucketsResponse { + reward_buckets: claimable_reward_buckets, }) } diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 120ae7cb..e268ea75 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,7 +1,7 @@ use crate::ContractError; use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Uint128}; use cw_storage_plus::{Item, Map}; -use white_whale_std::bonding_manager::{Bond, Config, Epoch, GlobalIndex}; +use white_whale_std::bonding_manager::{Bond, Config, GlobalIndex, RewardBucket}; type Denom = str; @@ -11,7 +11,7 @@ pub const BOND: Map<(&Addr, &Denom), Bond> = Map::new("bond"); pub const UNBOND: Map<(&Addr, &Denom, u64), Bond> = Map::new("unbond"); pub const GLOBAL: Item = Item::new("global"); pub const LAST_CLAIMED_EPOCH: Map<&Addr, u64> = Map::new("last_claimed_epoch"); -pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); +pub const REWARD_BUCKETS: Map = Map::new("reward_buckets"); /// Updates the local weight of the given address. pub fn update_local_weight( @@ -31,11 +31,7 @@ pub fn update_local_weight( )?; bond.updated_last = current_epoch_id; - - let denom: &String = &bond.asset.denom; - - //todo remove? done outside of this function. Or remove outside - BOND.save(deps.storage, (&address, denom), &bond)?; + BOND.save(deps.storage, (&address, &bond.asset.denom), &bond)?; Ok(bond) } @@ -57,8 +53,6 @@ pub fn update_global_weight( )?; global_index.last_updated = current_epoch_id; - - //todo remove? done outside of this function. Or remove outside GLOBAL.save(deps.storage, &global_index)?; Ok(global_index) @@ -87,18 +81,18 @@ pub fn get_weight( /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. -pub fn get_expiring_epoch(deps: Deps) -> StdResult> { +pub fn get_expiring_epoch(deps: Deps) -> StdResult> { let grace_period = CONFIG.load(deps.storage)?.grace_period; // last epochs within the grace period - let epochs = EPOCHS + let epochs = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) .take(grace_period as usize) .map(|item| { let (_, epoch) = item?; Ok(epoch) }) - .collect::>>()?; + .collect::>>()?; // if the epochs vector's length is the same as the grace period it means there is one epoch that // is expiring once the new one is created i.e. the last epoch in the vector diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 3eb472bd..dc632af5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -11,13 +11,13 @@ use cw_multi_test::{ use white_whale_std::fee::PoolFee; use white_whale_testing::multi_test::stargate_mock::StargateMock; -use crate::state::{CONFIG, EPOCHS}; +use crate::state::{CONFIG, REWARD_BUCKETS}; use cw_multi_test::{Contract, ContractWrapper}; use white_whale_std::bonding_manager::{ BondedResponse, BondingWeightResponse, Config, ExecuteMsg, InstantiateMsg, QueryMsg, UnbondingResponse, WithdrawableResponse, }; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; +use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; use white_whale_std::pool_manager::PoolType; @@ -370,9 +370,9 @@ impl TestingSuite { self } - pub(crate) fn add_epochs_to_state(&mut self, epochs: Vec) -> &mut Self { + pub(crate) fn add_epochs_to_state(&mut self, epochs: Vec) -> &mut Self { for epoch in epochs { - EPOCHS + REWARD_BUCKETS .save( &mut self.owned_deps.storage, &epoch.id.to_be_bytes(), @@ -470,7 +470,7 @@ impl TestingSuite { pub(crate) fn query_claimable_epochs( &mut self, address: Option, - response: impl Fn(StdResult<(&mut Self, Vec)>), + response: impl Fn(StdResult<(&mut Self, Vec)>), ) -> &mut Self { let address = if let Some(address) = address { Some(address.to_string()) @@ -478,13 +478,13 @@ impl TestingSuite { None }; - let query_res: ClaimableEpochsResponse = self + let query_res: ClaimableRewardBucketsResponse = self .app .wrap() .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::Claimable { address }) .unwrap(); - response(Ok((self, query_res.epochs))); + response(Ok((self, query_res.reward_buckets))); self } diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 953a2404..fb1b5030 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -1,4 +1,4 @@ -use crate::epoch_manager::epoch_manager::Epoch as EpochV2; +use crate::epoch_manager::epoch_manager::Epoch; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ @@ -22,25 +22,26 @@ pub struct Config { pub growth_rate: Decimal, /// Denom of the asset to be bonded. Can't only be set at instantiation. pub bonding_assets: Vec, - /// Grace period the maximum age of a epoch bucket before it's considered expired and fees + /// Grace period the maximum age of a reward bucket before it's considered expired and fees /// are forwarded from it pub grace_period: u64, } #[cw_serde] #[derive(Default)] -pub struct Epoch { - // Epoch identifier +pub struct RewardBucket { + // id of the reward bucket. Matches the epoch id it's associated with pub id: u64, // Epoch start time - pub start_time: Timestamp, - // Initial fees to be distributed in this epoch. + pub epoch_start_time: Timestamp, + // Initial fees to be distributed in this reward bucket. pub total: Vec, - // Fees left to be claimed on this epoch. These available fees are forwarded when the epoch expires. + // Fees left to be claimed on this reward bucket. These available fees are forwarded when the + // reward bucket expires. pub available: Vec, - // Fees that were claimed on this epoch. For keeping record on the total fees claimed. + // Fees that were claimed on this reward bucket. For keeping record on the total fees claimed. pub claimed: Vec, - // Global index taken at the time of Epoch Creation + // Global index snapshot taken at the time of reward bucket creation pub global_index: GlobalIndex, } @@ -94,7 +95,7 @@ pub struct InstantiateMsg { pub growth_rate: Decimal, /// [String] denoms of the assets that can be bonded. pub bonding_assets: Vec, - /// Grace period the maximum age of a epoch bucket before it's considered expired and fees + /// Grace period the maximum age of a reward bucket before it's considered expired and fees /// are forwarded from it pub grace_period: u64, /// The epoch manager contract @@ -103,7 +104,7 @@ pub struct InstantiateMsg { #[cw_serde] pub struct EpochChangedHookMsg { - pub current_epoch: EpochV2, + pub current_epoch: Epoch, } #[cw_ownable_execute] @@ -138,11 +139,12 @@ pub enum ExecuteMsg { /// Fills the contract with new rewards. FillRewards, - /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in - /// the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + /// Epoch Changed hook implementation. Creates a new reward bucket for the rewards flowing from + /// this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring + /// reward bucket (only 21 of them are live at a given moment) EpochChangedHook { /// The current epoch, the one that was newly created. - current_epoch: EpochV2, + current_epoch: Epoch, }, } @@ -201,11 +203,11 @@ pub enum QueryMsg { #[returns(GlobalIndex)] GlobalIndex, - /// Returns the [Epoch]s that can be claimed by an address. - #[returns(ClaimableEpochsResponse)] + /// Returns the [RewardBucket]s that can be claimed by an address. + #[returns(ClaimableRewardBucketsResponse)] Claimable { - /// The address to check for claimable epochs. If none is provided, all possible epochs - /// stored in the contract that can potentially be claimed are returned. + /// The address to check for claimable reward buckets. If none is provided, all possible + /// reward buckets stored in the contract that can potentially be claimed are returned. address: Option, }, } @@ -267,7 +269,7 @@ pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult, +pub struct ClaimableRewardBucketsResponse { + /// The reward buckets that can be claimed by the address. + pub reward_buckets: Vec, } From 7e4b523b196fa398fa1bd2039f688756371ec680 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 13 May 2024 14:20:45 +0100 Subject: [PATCH 26/51] chore: start point before major remake of the bonding mechanism --- .../schema/bonding-manager.json | 57 ++++- .../bonding-manager/schema/raw/query.json | 35 ++- .../schema/raw/response_to_claimable.json | 11 +- .../schema/raw/response_to_global_index.json | 11 +- .../bonding-manager/src/commands.rs | 44 ++-- .../bonding-manager/src/contract.rs | 4 +- .../bonding-manager/src/error.rs | 4 +- .../bonding-manager/src/helpers.rs | 27 +-- .../bonding-manager/src/queries.rs | 42 +++- .../bonding-manager/src/state.rs | 10 +- .../bonding-manager/src/tests/bond.rs | 229 +++++++++++++++--- .../bonding-manager/src/tests/instantiate.rs | 10 +- .../bonding-manager/src/tests/suite.rs | 121 ++++----- .../src/tests/update_config.rs | 30 +-- .../epoch-manager/src/commands.rs | 10 +- .../white-whale-std/src/bonding_manager.rs | 11 +- 16 files changed, 464 insertions(+), 192 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index bdc8c4e2..0adaf341 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -532,10 +532,28 @@ }, { "description": "Returns the global index of the contract.", - "type": "string", - "enum": [ + "type": "object", + "required": [ "global_index" - ] + ], + "properties": { + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false }, { "description": "Returns the [RewardBucket]s that can be claimed by an address.", @@ -596,7 +614,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -615,7 +634,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", @@ -742,7 +767,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -761,7 +787,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", @@ -920,7 +952,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -939,7 +972,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index 7139a19f..25ad45d2 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -151,10 +151,28 @@ }, { "description": "Returns the global index of the contract.", - "type": "string", - "enum": [ + "type": "object", + "required": [ "global_index" - ] + ], + "properties": { + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false }, { "description": "Returns the [RewardBucket]s that can be claimed by an address.", @@ -215,7 +233,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -234,7 +253,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", 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 e05fe961..351a5b7a 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 @@ -36,7 +36,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -55,7 +56,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", 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 4128e977..66367c40 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 @@ -5,7 +5,8 @@ "required": [ "bonded_amount", "bonded_assets", - "last_updated", + "epoch_id", + "updated_last", "weight" ], "properties": { @@ -24,7 +25,13 @@ "$ref": "#/definitions/Coin" } }, - "last_updated": { + "epoch_id": { + "description": "The epoch id the global index was taken a snapshot for", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "updated_last": { "description": "The epoch id at which the total bond was updated.", "type": "integer", "format": "uint64", diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 697ab5a1..a2cb7eaf 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -10,7 +10,7 @@ use white_whale_std::pool_network::asset; use crate::helpers::validate_growth_rate; use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight, MAX_PAGE_LIMIT}; use crate::state::{ - update_global_weight, update_local_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, + update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, UNBOND, }; use crate::{helpers, ContractError}; @@ -19,14 +19,13 @@ use crate::{helpers, ContractError}; pub(crate) fn bond( mut deps: DepsMut, info: MessageInfo, - env: Env, + _env: Env, asset: Coin, ) -> Result { - println!("bonding"); + println!("----bond----"); helpers::validate_buckets(&deps)?; helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps, &env)?; - println!("bonding 2"); + helpers::validate_bonding_for_current_epoch(&deps)?; let config = CONFIG.load(deps.storage)?; let current_epoch: white_whale_std::epoch_manager::epoch_manager::EpochResponse = @@ -44,17 +43,19 @@ pub(crate) fn bond( ..asset.clone() }, created_at_epoch: current_epoch.epoch.id, + updated_last: current_epoch.epoch.id, ..Bond::default() }); // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; - update_local_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; + update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); // include time term in the weight + global_index.weight = global_index.weight.checked_add(asset.amount)?; global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; global_index.bonded_assets = @@ -81,7 +82,7 @@ pub(crate) fn unbond( ); helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps, &env)?; + helpers::validate_bonding_for_current_epoch(&deps)?; if let Some(mut unbond) = BOND .key((&info.sender, &asset.denom)) .may_load(deps.storage)? @@ -100,7 +101,7 @@ pub(crate) fn unbond( )?; // update local values, decrease the bond - unbond = update_local_weight( + unbond = update_bond_weight( &mut deps, info.sender.clone(), current_epoch.epoch.id, @@ -364,12 +365,7 @@ pub(crate) fn fill_rewards( env: Env, info: MessageInfo, ) -> Result { - println!( - "EPOCHS: {:?}", - REWARD_BUCKETS - .keys(deps.storage, None, None, Order::Descending) - .collect::>() - ); + println!("----fill_rewards----"); // Finding the most recent bucket let upcoming_bucket_id = match REWARD_BUCKETS @@ -406,7 +402,6 @@ pub(crate) fn fill_rewards( .filter(|coin| coin.denom.ne(distribution_denom.as_str())) .collect::>(); - println!("remanent_coins: {:?}", remanent_coins); // Each of these helpers will add messages to the messages vector // and may increment the whale Coin above with the result of the swaps helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; @@ -419,7 +414,6 @@ pub(crate) fn fill_rewards( &mut messages, )?; - println!("here"); // Add the whale to the funds, the whale figure now should be the result // of all the LP token withdrawals and swaps // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned @@ -443,6 +437,7 @@ pub(crate) fn on_epoch_created( ) -> Result { cw_utils::nonpayable(&info)?; + println!("----on_epoch_created----"); println!("EpochChangedHook: {:?}", current_epoch); // A new epoch has been created, update rewards bucket and forward the expiring bucket let config = CONFIG.load(deps.storage)?; @@ -455,7 +450,7 @@ pub(crate) fn on_epoch_created( // This happens only on the very first epoch where Global has not been initialised yet if global.is_none() { let initial_global_index = GlobalIndex { - last_updated: current_epoch.id, + updated_last: current_epoch.id, ..Default::default() }; GLOBAL.save(deps.storage, &initial_global_index)?; @@ -471,7 +466,9 @@ pub(crate) fn on_epoch_created( )?; } - let global = GLOBAL.load(deps.storage)?; + // Update the global index epoch id field + let mut global = GLOBAL.load(deps.storage)?; + global.epoch_id = current_epoch.id; // update the global index for the current bucket, take the current snapshot of the global index REWARD_BUCKETS.update( @@ -484,17 +481,6 @@ pub(crate) fn on_epoch_created( }, )?; - // todo to delete once the testing is done - let all_buckets: Vec = REWARD_BUCKETS - .range(deps.storage, None, None, Order::Descending) - .map(|item| { - let (_, bucket) = item?; - Ok(bucket) - }) - .collect::>>()?; - - println!("EPOCHS: {:?}", all_buckets); - // forward fees from the expiring bucket to the new one. let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index a903f65c..ecb27431 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -148,7 +148,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary(&queries::query_global_index(deps)?)?), + QueryMsg::GlobalIndex { epoch_id } => Ok(to_json_binary(&queries::query_global_index( + deps, epoch_id, + )?)?), QueryMsg::Claimable { address } => { Ok(to_json_binary(&queries::query_claimable(deps, address)?)?) } diff --git a/contracts/liquidity_hub/bonding-manager/src/error.rs b/contracts/liquidity_hub/bonding-manager/src/error.rs index a3be149b..43f8e65c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/error.rs +++ b/contracts/liquidity_hub/bonding-manager/src/error.rs @@ -59,8 +59,8 @@ pub enum ContractError { #[error("There are unclaimed rewards available. Claim them before attempting to bond/unbond")] UnclaimedRewards, - #[error("Trying to bond/unbond at a late time before the new/latest epoch has been created")] - NewEpochNotCreatedYet, + #[error("Trying to bond before an epoch has been created")] + EpochNotCreatedYet, #[error("Nothing to claim")] NothingToClaim, diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index aeac395b..16af187e 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,10 +1,10 @@ use cosmwasm_std::{ - ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, ReplyOn, + ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, MessageInfo, Order, ReplyOn, StdResult, SubMsg, WasmMsg, }; use cw_utils::PaymentError; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, Config}; -use white_whale_std::constants::{DAY_IN_SECONDS, LP_SYMBOL}; +use white_whale_std::constants::LP_SYMBOL; use white_whale_std::epoch_manager::epoch_manager::EpochResponse; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, @@ -68,33 +68,16 @@ 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, env: &Env) -> Result<(), ContractError> { +pub fn validate_bonding_for_current_epoch(deps: &DepsMut) -> 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 current_epoch = epoch_response.epoch; - // Check if the current time is more than a day after the epoch start time - if current_epoch.id != 0u64 { - let current_time = env.block.time.seconds(); + let reward_bucket = REWARD_BUCKETS.may_load(deps.storage, epoch_response.epoch.id)?; - let start_time_seconds = current_epoch - .start_time - .seconds() - .checked_add(DAY_IN_SECONDS); - - match start_time_seconds { - Some(start_time_plus_day) => { - ensure!( - current_time <= start_time_plus_day, - ContractError::NewEpochNotCreatedYet - ); - } - None => return Err(ContractError::Unauthorized), - } - } + ensure!(reward_bucket.is_some(), ContractError::EpochNotCreatedYet); Ok(()) } diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 73420d76..2c8b1c01 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -161,7 +161,15 @@ 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 current_epoch: white_whale_std::epoch_manager::epoch_manager::EpochResponse = + deps.querier.query_wasm_smart( + config.epoch_manager_addr, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; let mut total_bond_weight = Uint128::zero(); // Search bonds for unique bond.asset.denoms @@ -169,6 +177,17 @@ pub(crate) fn query_weight( let mut unique_denoms: HashSet = HashSet::new(); for (_, mut bond) in bonds? { + println!("bond-before: {:?}", bond); + + // if bond.updated_last == current_epoch.epoch.id { + // // take previous value + // } + + //todo + if bond.created_at_epoch == current_epoch.epoch.id { + continue; + } + bond.weight = get_weight( epoch_id, bond.weight, @@ -177,32 +196,41 @@ pub(crate) fn query_weight( bond.updated_last, )?; + println!("bond-after: {:?}", bond); + if !unique_denoms.contains(&bond.asset.denom) { unique_denoms.insert(bond.asset.denom.clone()); + println!("unique_denoms: {:?}", unique_denoms); } // 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.weight = get_weight( epoch_id, global_index.weight, global_index.bonded_amount, config.growth_rate, - global_index.last_updated, + global_index.updated_last, )?; + 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.weight.is_zero() { @@ -221,7 +249,15 @@ pub(crate) fn query_weight( } /// Queries the global index -pub fn query_global_index(deps: Deps) -> StdResult { +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); + } + } + let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); Ok(global_index) } @@ -335,6 +371,8 @@ pub fn query_claimable( claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } + // todo exclude buckets which eopch id is the same as the bond created epoch id + Ok(ClaimableRewardBucketsResponse { reward_buckets: claimable_reward_buckets, }) diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index e268ea75..72705f79 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -14,7 +14,7 @@ pub const LAST_CLAIMED_EPOCH: Map<&Addr, u64> = Map::new("last_claimed_epoch"); pub const REWARD_BUCKETS: Map = Map::new("reward_buckets"); /// Updates the local weight of the given address. -pub fn update_local_weight( +pub fn update_bond_weight( deps: &mut DepsMut, address: Addr, current_epoch_id: u64, @@ -33,6 +33,8 @@ pub fn update_local_weight( bond.updated_last = current_epoch_id; BOND.save(deps.storage, (&address, &bond.asset.denom), &bond)?; + println!("updated bond: {:?}", bond); + Ok(bond) } @@ -49,12 +51,14 @@ pub fn update_global_weight( global_index.weight, global_index.bonded_amount, config.growth_rate, - global_index.last_updated, + global_index.updated_last, )?; - global_index.last_updated = current_epoch_id; + global_index.updated_last = current_epoch_id; GLOBAL.save(deps.storage, &global_index)?; + println!("updated global_index: {:?}", global_index); + Ok(global_index) } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index d4b6de33..dd50b684 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -1,9 +1,10 @@ -use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{coins, Coin, Decimal, Uint128}; +use std::cell::RefCell; -use crate::ContractError; -use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse}; +use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse, GlobalIndex}; use crate::tests::suite::TestingSuite; +use crate::ContractError; #[test] fn test_bond_successfully() { @@ -11,6 +12,8 @@ fn test_bond_successfully() { let sender = suite.sender.clone(); let another_sender = suite.another_sender.clone(); + let global_index = RefCell::new(GlobalIndex::default()); + suite .instantiate_default() .bond( @@ -42,7 +45,13 @@ fn test_bond_successfully() { suite .add_one_day() + // created epoch 1 .create_new_epoch() + .query_global_index(Some(1u64), |res| { + let gi = res.unwrap().1; + *global_index.borrow_mut() = gi.clone(); + println!("gi 1:: {:?}", gi); + }) .bond( sender.clone(), Coin { @@ -62,17 +71,19 @@ fn test_bond_successfully() { denom: "ampWHALE".to_string(), amount: Uint128::new(1_000u128), }], - first_bonded_epoch_id: Some(Uint64::new(1u64)), + first_bonded_epoch_id: Some(1u64), }, ) .assert_bonding_weight_response( sender.to_string(), + Some(1u64), + Some(global_index.clone().into_inner()), BondingWeightResponse { address: sender.to_string(), weight: Uint128::new(1_000u128), - global_weight: Uint128::new(1_000u128), - share: Decimal::one(), - epoch_id: Timestamp::from_nanos(1571883819879305533u64), + global_weight: Uint128::zero(), // because the snapshot was taken at the beginning of the epoch + share: Decimal::zero(), + epoch_id: 1u64, }, ); @@ -103,13 +114,103 @@ fn test_bond_successfully() { amount: Uint128::new(3_000u128), }, ], - first_bonded_epoch_id: Some(Uint64::new(1u64)), + first_bonded_epoch_id: Some(1u64), }, ); suite .add_one_day() + // epoch 2 .create_new_epoch() + .query_global_index(Some(2u64), |res| { + let gi = res.unwrap().1; + println!("gi 2:: {:?}", gi); + *global_index.borrow_mut() = gi.clone(); + }); + + suite + .query_weight( + sender.to_string(), + Some(2u64), + Some(global_index.clone().into_inner()), + |res| { + let bonded_response = res.unwrap().1; + println!("bonded_response 1:: {:?}", bonded_response); + }, + ) + .bond( + sender.clone(), + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + &coins(1_000u128, "bWHALE"), + |result| { + result.unwrap(); + }, + ) + .assert_bonded_response( + sender.to_string(), + BondedResponse { + total_bonded: Uint128::new(5_000u128), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(4_000u128), + }, + ], + first_bonded_epoch_id: Some(1u64), + }, + ); + + println!( + "herrreeee global_index:: {:?}", + global_index.clone().into_inner() + ); + + suite + .assert_bonding_weight_response( + sender.to_string(), + Some(2u64), + Some(global_index.clone().into_inner()), + BondingWeightResponse { + address: sender.to_string(), + weight: Uint128::new(10000u128), + global_weight: Uint128::new(10000u128), + share: Decimal::from_ratio(10000u128, 10000u128), + epoch_id: 2u64, + }, + ) + .query_weight(sender.to_string(), Some(2u64), None, |res| { + let bonded_response = res.unwrap().1; + println!("bonded_response 2:: {:?}", bonded_response); + }); + + suite + .query_bonded(None, |res| { + let bonded_response = res.unwrap().1; + assert_eq!( + bonded_response, + BondedResponse { + total_bonded: Uint128::new(5_000u128), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(4_000u128), + }, + ], + first_bonded_epoch_id: None, + } + ) + }) .bond( another_sender.clone(), Coin { @@ -127,17 +228,19 @@ fn test_bond_successfully() { denom: "ampWHALE".to_string(), amount: Uint128::new(5_000u128), }], - first_bonded_epoch_id: Some(Uint64::new(2u64)), + first_bonded_epoch_id: Some(2u64), }, ) .assert_bonding_weight_response( another_sender.to_string(), + Some(2u64), + Some(global_index.clone().into_inner()), BondingWeightResponse { address: another_sender.to_string(), weight: Uint128::new(5_000u128), - global_weight: Uint128::new(950_409_000u128), - share: Decimal::from_ratio(5_000u128, 950_409_000u128), - epoch_id: Timestamp::from_nanos(1572013419879305533u64), + global_weight: Uint128::new(15_000u128), + share: Decimal::from_ratio(5_000u128, 15_000u128), + epoch_id: 2u64, }, ) .query_bonded(None, |res| { @@ -145,7 +248,7 @@ fn test_bond_successfully() { assert_eq!( bonded_response, BondedResponse { - total_bonded: Uint128::new(9_000u128), + total_bonded: Uint128::new(10_000u128), bonded_assets: vec![ Coin { denom: "ampWHALE".to_string(), @@ -153,38 +256,104 @@ fn test_bond_successfully() { }, Coin { denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), + amount: Uint128::new(4_000u128), }, ], first_bonded_epoch_id: None, } ) + }) + .query_weight(sender.to_string(), Some(2u64), None, |res| { + let bonded_response = res.unwrap().1; + println!("bonded_response sender:: {:?}", bonded_response); + }) + .query_weight(another_sender.to_string(), Some(2u64), None, |res| { + let bonded_response = res.unwrap().1; + println!("bonded_response another_sender:: {:?}", bonded_response); }); suite .add_one_day() - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(734_404_000u128), - global_weight: Uint128::new(1_728_009_000u128), - share: Decimal::from_ratio(734_404_000u128, 1_728_009_000u128), - epoch_id: Timestamp::from_nanos(1572099819879305533u64), + .create_new_epoch() + .query_global_index(Some(3u64), |res| { + let gi = res.unwrap().1; + *global_index.borrow_mut() = gi.clone(); + println!("gi:: {:?}", gi); + }) + .query_weight(sender.to_string(), Some(3u64), None, |res| { + let bonded_response = res.unwrap().1; + println!("bonded_response sender again:: {:?}", bonded_response); + }) + .query_weight(another_sender.to_string(), Some(3u64), None, |res| { + let bonded_response = res.unwrap().1; + println!( + "bonded_response another_sender again:: {:?}", + bonded_response + ); + }); + + suite.assert_bonding_weight_response( + another_sender.to_string(), + Some(3u64), + Some(global_index.clone().into_inner()), + BondingWeightResponse { + address: another_sender.to_string(), + weight: Uint128::new(10_000u128), + global_weight: Uint128::new(25_000u128), + share: Decimal::from_ratio(10_000u128, 25_000u128), + epoch_id: 3u64, + }, + ); + + suite + .bond( + another_sender.clone(), + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(2_000u128), + }, + &coins(2_000u128, "bWHALE"), + |_res| {}, + ) + .assert_bonded_response( + another_sender.to_string(), + BondedResponse { + total_bonded: Uint128::new(7_000u128), + bonded_assets: vec![ + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(5_000u128), + }, + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(2_000u128), + }, + ], + first_bonded_epoch_id: Some(2u64), }, ) .assert_bonding_weight_response( another_sender.to_string(), + Some(3u64), + Some(global_index.clone().into_inner()), BondingWeightResponse { address: another_sender.to_string(), - weight: Uint128::new(432_005_000u128), - global_weight: Uint128::new(1_728_009_000u128), - share: Decimal::from_ratio(432_005_000u128, 1_728_009_000u128), - epoch_id: Timestamp::from_nanos(1572099819879305533u64), + weight: Uint128::new(12_000u128), + global_weight: Uint128::new(29_000u128), + share: Decimal::from_ratio(12_000u128, 29_000u128), + epoch_id: 3u64, }, ) - .query_bonded(None, |result| { - let res = result.unwrap().1; - println!("{:?}", res); - }); + .assert_bonding_weight_response( + sender.to_string(), + Some(3u64), + Some(global_index.clone().into_inner()), + BondingWeightResponse { + address: sender.to_string(), + weight: Uint128::new(15_000u128), + global_weight: Uint128::new(29_000u128), + share: Decimal::from_ratio(15_000u128, 29_000u128), + epoch_id: 3u64, + }, + ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index f02c7cab..7516ffa1 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -12,7 +12,7 @@ fn test_instantiate_successfully() { suite .instantiate( - Uint64::new(1_000u64), + 1u64, Decimal::one(), vec!["ampWHALE".to_string(), "bWHALE".to_string()], &vec![], @@ -21,9 +21,9 @@ fn test_instantiate_successfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000u64), + unbonding_period: 1u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }); } @@ -35,7 +35,7 @@ fn test_instantiate_unsuccessfully() { // over bonding assets limit suite .instantiate_err( - Uint64::new(1_000u64), + 1u64, Decimal::one(), vec![ "ampWHALE".to_string(), @@ -51,7 +51,7 @@ fn test_instantiate_unsuccessfully() { }, ) .instantiate_err( - Uint64::new(1_000u64), + 1u64, Decimal::percent(200), vec!["ampWHALE".to_string(), "bWHALE".to_string()], &vec![], diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index dc632af5..5e9a6e49 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -14,8 +14,8 @@ 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, InstantiateMsg, QueryMsg, - UnbondingResponse, WithdrawableResponse, + BondedResponse, BondingWeightResponse, Config, ExecuteMsg, GlobalIndex, InstantiateMsg, + QueryMsg, UnbondingResponse, WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; @@ -82,6 +82,7 @@ pub struct TestingSuite { /// instantiate / execute messages impl TestingSuite { + #[track_caller] pub(crate) fn default() -> Self { let sender = Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); let another_sender = Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"); @@ -125,6 +126,7 @@ impl TestingSuite { } } + #[track_caller] pub(crate) fn fast_forward(&mut self, seconds: u64) -> &mut Self { let mut block_info = self.app.block_info(); block_info.time = block_info.time.plus_nanos(seconds * 1_000_000_000); @@ -132,7 +134,7 @@ impl TestingSuite { self } - + #[track_caller] pub(crate) fn add_one_day(&mut self) -> &mut Self { let mut block_info = self.app.block_info(); block_info.time = block_info.time.plus_days(1); @@ -141,24 +143,26 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn instantiate_default(&mut self) -> &mut Self { self.instantiate( - Uint64::new(86_400_000000000u64), + 1u64, Decimal::one(), vec!["ampWHALE".to_string(), "bWHALE".to_string()], &vec![], ) } + #[track_caller] pub(crate) fn instantiate( &mut self, - unbonding_period: Uint64, + unbonding_period: u64, growth_rate: Decimal, bonding_assets: Vec, funds: &Vec, ) -> &mut Self { let epoch_manager_id = self.app.store_code(epoch_manager_contract()); - println!("epoch_manager_id: {}", self.app.block_info().time.nanos()); + let epoch_manager_addr = self .app .instantiate_contract( @@ -183,7 +187,6 @@ impl TestingSuite { let bonding_manager_addr = instantiate_contract(self, unbonding_period, growth_rate, bonding_assets, funds) .unwrap(); - println!("bonding_manager_addr: {}", bonding_manager_addr); let hook_registration_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::AddHook { @@ -239,9 +242,10 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn instantiate_err( &mut self, - unbonding_period: Uint64, + unbonding_period: u64, growth_rate: Decimal, bonding_assets: Vec, funds: &Vec, @@ -255,6 +259,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn bond( &mut self, sender: Addr, @@ -272,6 +277,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn unbond( &mut self, sender: Addr, @@ -288,6 +294,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn claim( &mut self, sender: Addr, @@ -303,6 +310,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn withdraw( &mut self, sender: Addr, @@ -319,6 +327,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn create_new_epoch(&mut self) -> &mut Self { let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; self.app @@ -346,12 +355,13 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn update_config( &mut self, sender: Addr, epoch_manager_addr: Option, pool_manager_addr: Option, - unbonding_period: Option, + unbonding_period: Option, growth_rate: Option, response: impl Fn(Result), ) -> &mut Self { @@ -369,25 +379,11 @@ impl TestingSuite { self } - - pub(crate) fn add_epochs_to_state(&mut self, epochs: Vec) -> &mut Self { - for epoch in epochs { - REWARD_BUCKETS - .save( - &mut self.owned_deps.storage, - &epoch.id.to_be_bytes(), - &epoch, - ) - .unwrap(); - } - - self - } } fn instantiate_contract( - robot: &mut TestingSuite, - unbonding_period: Uint64, + suite: &mut TestingSuite, + unbonding_period: u64, growth_rate: Decimal, bonding_assets: Vec, funds: &Vec, @@ -397,23 +393,24 @@ fn instantiate_contract( distribution_denom: "uwhale".to_string(), growth_rate, bonding_assets, - grace_period: Uint64::new(21), + grace_period: 21u64, epoch_manager_addr: "".to_string(), }; - let bonding_manager_id = robot.app.store_code(bonding_manager_contract()); - robot.app.instantiate_contract( + let bonding_manager_id = suite.app.store_code(bonding_manager_contract()); + suite.app.instantiate_contract( bonding_manager_id, - robot.sender.clone(), + suite.sender.clone(), &msg, funds, "Bonding Manager".to_string(), - Some(robot.sender.clone().to_string()), + Some(suite.sender.clone().to_string()), ) } /// queries impl TestingSuite { + #[track_caller] pub(crate) fn query_config( &mut self, response: impl Fn(StdResult<(&mut Self, Config)>), @@ -428,7 +425,7 @@ impl TestingSuite { self } - + #[track_caller] pub(crate) fn query_owner( &mut self, response: impl Fn(StdResult<(&mut Self, String)>), @@ -444,9 +441,12 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn query_weight( &mut self, address: String, + epoch_id: Option, + global_index: Option, response: impl Fn(StdResult<(&mut Self, BondingWeightResponse)>), ) -> &mut Self { let bonding_weight_response: BondingWeightResponse = self @@ -456,8 +456,8 @@ impl TestingSuite { &self.bonding_manager_addr, &QueryMsg::Weight { address, - epoch_id: Some(self.app.block_info().time), - global_index: None, + epoch_id, + global_index, }, ) .unwrap(); @@ -467,7 +467,28 @@ impl TestingSuite { self } - pub(crate) fn query_claimable_epochs( + #[track_caller] + pub(crate) fn query_global_index( + &mut self, + epoch_id: Option, + response: impl Fn(StdResult<(&mut Self, GlobalIndex)>), + ) -> &mut Self { + let global_index: GlobalIndex = self + .app + .wrap() + .query_wasm_smart( + &self.bonding_manager_addr, + &QueryMsg::GlobalIndex { epoch_id }, + ) + .unwrap(); + + response(Ok((self, global_index))); + + self + } + + #[track_caller] + pub(crate) fn query_claimable_reward_buckets( &mut self, address: Option, response: impl Fn(StdResult<(&mut Self, Vec)>), @@ -489,6 +510,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn query_bonded( &mut self, address: Option, @@ -505,6 +527,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn query_unbonding( &mut self, address: String, @@ -532,6 +555,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn query_withdrawable( &mut self, address: String, @@ -546,7 +570,6 @@ impl TestingSuite { &QueryMsg::Withdrawable { address, denom }, ) .unwrap(); - println!("withdrawable_response: {:?}", withdrawable_response); response(Ok((self, withdrawable_response))); @@ -671,27 +694,11 @@ impl TestingSuite { self } - - #[track_caller] - pub(crate) fn create_epoch( - &mut self, - result: impl Fn(Result), - ) -> &mut Self { - let sender = self.another_sender.clone(); - - result(self.app.execute_contract( - sender, - self.epoch_manager_addr.clone(), - &white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch, - &[], - )); - - self - } } /// assertions impl TestingSuite { + #[track_caller] pub(crate) fn assert_config(&mut self, expected: Config) -> &mut Self { self.query_config(|res| { let config = res.unwrap().1; @@ -701,6 +708,7 @@ impl TestingSuite { self } + #[track_caller] pub(crate) fn assert_owner(&mut self, expected: String) -> &mut Self { self.query_owner(|res| { let owner = res.unwrap().1; @@ -709,7 +717,7 @@ impl TestingSuite { self } - + #[track_caller] pub(crate) fn assert_bonded_response( &mut self, address: String, @@ -721,17 +729,21 @@ impl TestingSuite { }) } + #[track_caller] pub(crate) fn assert_bonding_weight_response( &mut self, address: String, + epoch_id: Option, + global_index: Option, expected: BondingWeightResponse, ) -> &mut Self { - self.query_weight(address, |res| { + self.query_weight(address, epoch_id, global_index, |res| { let bonding_weight_response = res.unwrap().1; assert_eq!(bonding_weight_response, expected); }) } + #[track_caller] pub(crate) fn assert_unbonding_response( &mut self, address: String, @@ -744,6 +756,7 @@ impl TestingSuite { }) } + #[track_caller] pub(crate) fn assert_withdrawable_response( &mut self, address: String, diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index 58175cc3..0ff9c00b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -16,9 +16,9 @@ fn test_update_config_successfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), + unbonding_period: 1u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) .assert_owner("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3".to_string()) @@ -26,7 +26,7 @@ fn test_update_config_successfully() { owner.clone(), None, None, - Some(Uint64::new(500u64)), + Some(500u64), Some(Decimal::from_ratio( Uint128::new(1u128), Uint128::new(2u128), @@ -37,9 +37,9 @@ fn test_update_config_successfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(500u64), + unbonding_period: 500u64, growth_rate: Decimal::from_ratio(Uint128::new(1u128), Uint128::new(2u128)), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) .update_config( @@ -54,9 +54,9 @@ fn test_update_config_successfully() { pool_manager_addr: Addr::unchecked("contract6"), epoch_manager_addr: Addr::unchecked("contract5"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(500u64), + unbonding_period: 500u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }); } @@ -72,16 +72,16 @@ fn test_update_config_unsuccessfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), + unbonding_period: 1u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) .update_config( Addr::unchecked("unauthorized"), None, None, - Some(Uint64::new(500u64)), + Some(500u64), Some(Decimal::from_ratio( Uint128::new(1u128), Uint128::new(2u128), @@ -100,16 +100,16 @@ fn test_update_config_unsuccessfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), + unbonding_period: 1u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }) .update_config( owner, None, None, - Some(Uint64::new(500u64)), + Some(500u64), Some(Decimal::from_ratio( Uint128::new(2u128), Uint128::new(1u128), @@ -128,9 +128,9 @@ fn test_update_config_unsuccessfully() { pool_manager_addr: Addr::unchecked("contract2"), epoch_manager_addr: Addr::unchecked("contract0"), distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), + unbonding_period: 1u64, growth_rate: Decimal::one(), - grace_period: Uint64::new(21u64), + grace_period: 21u64, bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], }); } diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index 61fcb26f..93be93e2 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -32,15 +32,7 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= current_epoch.start_time, diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index fb1b5030..faa0e883 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -55,6 +55,7 @@ pub struct Bond { pub updated_last: u64, /// The weight of the bond at the given block height. pub weight: Uint128, + //pub previous: (u64, Uint128) } impl Default for Bond { @@ -74,12 +75,14 @@ impl Default for Bond { #[cw_serde] #[derive(Default)] pub struct GlobalIndex { + /// The epoch id the global index was taken a snapshot for + pub epoch_id: u64, /// The total amount of tokens bonded in the contract. pub bonded_amount: Uint128, /// Assets that are bonded in the contract. pub bonded_assets: Vec, /// The epoch id at which the total bond was updated. - pub last_updated: u64, + pub updated_last: u64, /// The total weight of the bond at the given block height. pub weight: Uint128, } @@ -201,7 +204,11 @@ pub enum QueryMsg { /// Returns the global index of the contract. #[returns(GlobalIndex)] - GlobalIndex, + GlobalIndex { + /// The epoch id to check for the global index. If none is provided, the current global index + /// is returned. + epoch_id: Option, + }, /// Returns the [RewardBucket]s that can be claimed by an address. #[returns(ClaimableRewardBucketsResponse)] From 1f5d72235f3282320bb6e8cbe3ce0d139e1aca36 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 13 May 2024 14:27:09 +0100 Subject: [PATCH 27/51] refactor: move functions to proper folders --- .../bonding-manager/src/bonding/commands.rs | 192 +++++++ .../bonding-manager/src/commands.rs | 486 +----------------- .../bonding-manager/src/contract.rs | 14 +- .../bonding-manager/src/distribution/mod.rs | 1 - .../liquidity_hub/bonding-manager/src/lib.rs | 2 + .../bonding-manager/src/rewards/commands.rs | 296 +++++++++++ .../src/{collection => rewards}/mod.rs | 0 .../bonding-manager/src/state.rs | 5 + 8 files changed, 505 insertions(+), 491 deletions(-) create mode 100644 contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs delete mode 100644 contracts/liquidity_hub/bonding-manager/src/distribution/mod.rs create mode 100644 contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs rename contracts/liquidity_hub/bonding-manager/src/{collection => rewards}/mod.rs (100%) diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs new file mode 100644 index 00000000..5de2f3b3 --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -0,0 +1,192 @@ +use crate::queries::MAX_PAGE_LIMIT; +use crate::state::{update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, UNBOND}; +use crate::{helpers, ContractError}; +use cosmwasm_std::{ + ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, + StdResult, Uint128, +}; +use white_whale_std::bonding_manager::Bond; +use white_whale_std::pool_network::asset; + +/// Bonds the provided asset. +pub(crate) fn bond( + mut deps: DepsMut, + info: MessageInfo, + _env: Env, + asset: Coin, +) -> Result { + println!("----bond----"); + helpers::validate_buckets(&deps)?; + helpers::validate_claimed(&deps, &info)?; + helpers::validate_bonding_for_current_epoch(&deps)?; + + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + let mut bond = BOND + .key((&info.sender, &asset.denom)) + .may_load(deps.storage)? + .unwrap_or(Bond { + asset: Coin { + amount: Uint128::zero(), + ..asset.clone() + }, + created_at_epoch: current_epoch.epoch.id, + updated_last: current_epoch.epoch.id, + ..Bond::default() + }); + + // update local values + bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; + bond.weight = bond.weight.checked_add(asset.amount)?; + update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; + + // update global values + let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); + // include time term in the weight + + global_index.weight = global_index.weight.checked_add(asset.amount)?; + global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; + global_index.bonded_assets = + asset::aggregate_coins(global_index.bonded_assets, vec![asset.clone()])?; + update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; + + Ok(Response::default().add_attributes(vec![ + ("action", "bond".to_string()), + ("address", info.sender.to_string()), + ("asset", asset.to_string()), + ])) +} + +/// Unbonds the provided amount of tokens +pub(crate) fn unbond( + mut deps: DepsMut, + info: MessageInfo, + env: Env, + asset: Coin, +) -> Result { + ensure!( + asset.amount > Uint128::zero(), + ContractError::InvalidUnbondingAmount + ); + + helpers::validate_claimed(&deps, &info)?; + helpers::validate_bonding_for_current_epoch(&deps)?; + if let Some(mut unbond) = BOND + .key((&info.sender, &asset.denom)) + .may_load(deps.storage)? + { + // check if the address has enough bond + ensure!( + unbond.asset.amount >= asset.amount, + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + // update local values, decrease the bond + unbond = update_bond_weight( + &mut deps, + info.sender.clone(), + current_epoch.epoch.id, + unbond.clone(), + )?; + let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); + unbond.weight = unbond.weight.saturating_sub(weight_slash); + unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); + + if unbond.asset.amount.is_zero() { + BOND.remove(deps.storage, (&info.sender, &asset.denom)); + } else { + BOND.save(deps.storage, (&info.sender, &asset.denom), &unbond)?; + } + + // record the unbonding + UNBOND.save( + deps.storage, + (&info.sender, &asset.denom, env.block.time.nanos()), + &Bond { + asset: asset.clone(), + weight: Uint128::zero(), + updated_last: current_epoch.epoch.id, + created_at_epoch: current_epoch.epoch.id, + }, + )?; + // update global values + let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); + global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; + global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); + global_index.bonded_assets = + white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; + global_index.weight = global_index.weight.saturating_sub(weight_slash); + + GLOBAL.save(deps.storage, &global_index)?; + + Ok(Response::default().add_attributes(vec![ + ("action", "unbond".to_string()), + ("address", info.sender.to_string()), + ("asset", asset.to_string()), + ])) + } else { + Err(ContractError::NothingToUnbond) + } +} + +/// Withdraws the rewards for the provided address +pub(crate) fn withdraw( + deps: DepsMut, + address: Addr, + denom: String, +) -> Result { + let unbondings: Vec<(u64, Bond)> = UNBOND + .prefix((&address, &denom)) + .range(deps.storage, None, None, Order::Ascending) + .take(MAX_PAGE_LIMIT as usize) + .collect::>>()?; + + ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); + + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + let mut refund_amount = Uint128::zero(); + for unbonding in unbondings { + let (ts, bond) = unbonding; + if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { + let denom = bond.asset.denom; + + refund_amount = refund_amount.checked_add(bond.asset.amount)?; + UNBOND.remove(deps.storage, (&address, &denom, ts)); + } + } + + let refund_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: address.to_string(), + amount: vec![Coin { + denom: denom.clone(), + amount: refund_amount, + }], + }); + + Ok(Response::default() + .add_message(refund_msg) + .add_attributes(vec![ + ("action", "withdraw".to_string()), + ("address", address.to_string()), + ("denom", denom), + ("refund_amount", refund_amount.to_string()), + ])) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index a2cb7eaf..7127df77 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -1,202 +1,8 @@ -use cosmwasm_std::{ - ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, - StdError, StdResult, SubMsg, Uint128, Uint64, -}; - -use white_whale_std::bonding_manager::{Bond, GlobalIndex, RewardBucket}; -use white_whale_std::epoch_manager::epoch_manager::Epoch; -use white_whale_std::pool_network::asset; +use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response}; use crate::helpers::validate_growth_rate; -use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight, MAX_PAGE_LIMIT}; -use crate::state::{ - update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, - REWARD_BUCKETS, UNBOND, -}; -use crate::{helpers, ContractError}; - -/// Bonds the provided asset. -pub(crate) fn bond( - mut deps: DepsMut, - info: MessageInfo, - _env: Env, - asset: Coin, -) -> Result { - println!("----bond----"); - helpers::validate_buckets(&deps)?; - helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps)?; - - 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; - - let mut bond = BOND - .key((&info.sender, &asset.denom)) - .may_load(deps.storage)? - .unwrap_or(Bond { - asset: Coin { - amount: Uint128::zero(), - ..asset.clone() - }, - created_at_epoch: current_epoch.epoch.id, - updated_last: current_epoch.epoch.id, - ..Bond::default() - }); - - // update local values - bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; - bond.weight = bond.weight.checked_add(asset.amount)?; - update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; - - // update global values - let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - // include time term in the weight - - global_index.weight = global_index.weight.checked_add(asset.amount)?; - global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; - global_index.bonded_assets = - asset::aggregate_coins(global_index.bonded_assets, vec![asset.clone()])?; - update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; - - Ok(Response::default().add_attributes(vec![ - ("action", "bond".to_string()), - ("address", info.sender.to_string()), - ("asset", asset.to_string()), - ])) -} - -/// Unbonds the provided amount of tokens -pub(crate) fn unbond( - mut deps: DepsMut, - info: MessageInfo, - env: Env, - asset: Coin, -) -> Result { - ensure!( - asset.amount > Uint128::zero(), - ContractError::InvalidUnbondingAmount - ); - - helpers::validate_claimed(&deps, &info)?; - helpers::validate_bonding_for_current_epoch(&deps)?; - if let Some(mut unbond) = BOND - .key((&info.sender, &asset.denom)) - .may_load(deps.storage)? - { - // check if the address has enough bond - ensure!( - unbond.asset.amount >= asset.amount, - 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; - - // update local values, decrease the bond - unbond = update_bond_weight( - &mut deps, - info.sender.clone(), - current_epoch.epoch.id, - unbond.clone(), - )?; - let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); - unbond.weight = unbond.weight.saturating_sub(weight_slash); - unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); - - if unbond.asset.amount.is_zero() { - BOND.remove(deps.storage, (&info.sender, &asset.denom)); - } else { - BOND.save(deps.storage, (&info.sender, &asset.denom), &unbond)?; - } - - // record the unbonding - UNBOND.save( - deps.storage, - (&info.sender, &asset.denom, env.block.time.nanos()), - &Bond { - asset: asset.clone(), - weight: Uint128::zero(), - updated_last: current_epoch.epoch.id, - created_at_epoch: current_epoch.epoch.id, - }, - )?; - // update global values - let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - global_index = update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; - global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); - global_index.bonded_assets = - white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index.weight = global_index.weight.saturating_sub(weight_slash); - - GLOBAL.save(deps.storage, &global_index)?; - - Ok(Response::default().add_attributes(vec![ - ("action", "unbond".to_string()), - ("address", info.sender.to_string()), - ("asset", asset.to_string()), - ])) - } else { - Err(ContractError::NothingToUnbond) - } -} - -/// Withdraws the rewards for the provided address -pub(crate) fn withdraw( - deps: DepsMut, - address: Addr, - denom: String, -) -> Result { - let unbondings: Vec<(u64, Bond)> = UNBOND - .prefix((&address, &denom)) - .range(deps.storage, None, None, Order::Ascending) - .take(MAX_PAGE_LIMIT as usize) - .collect::>>()?; - - ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); - - 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; - - let mut refund_amount = Uint128::zero(); - for unbonding in unbondings { - let (ts, bond) = unbonding; - if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { - let denom = bond.asset.denom; - - refund_amount = refund_amount.checked_add(bond.asset.amount)?; - UNBOND.remove(deps.storage, (&address, &denom, ts)); - } - } - - let refund_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: address.to_string(), - amount: vec![Coin { - denom: denom.clone(), - amount: refund_amount, - }], - }); - - Ok(Response::default() - .add_message(refund_msg) - .add_attributes(vec![ - ("action", "withdraw".to_string()), - ("address", address.to_string()), - ("denom", denom), - ("refund_amount", refund_amount.to_string()), - ])) -} +use crate::state::CONFIG; +use crate::ContractError; /// Updates the configuration of the contract pub(crate) fn update_config( @@ -239,289 +45,3 @@ pub(crate) fn update_config( ("growth_rate", config.growth_rate.to_string()), ])) } - -/// Claims pending rewards for the sender. -pub fn claim(deps: DepsMut, info: MessageInfo) -> Result { - let claimable_reward_buckets_for_user = - query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.reward_buckets; - ensure!( - !claimable_reward_buckets_for_user.is_empty(), - ContractError::NothingToClaim - ); - - let mut claimable_rewards = vec![]; - let mut attributes = vec![]; - for mut reward_bucket in claimable_reward_buckets_for_user.clone() { - let bonding_weight_response_for_epoch = query_weight( - deps.as_ref(), - reward_bucket.id, - info.sender.to_string(), - Some(reward_bucket.global_index.clone()), - )?; - - // if the user has no share in the bucket, skip it - if bonding_weight_response_for_epoch.share.is_zero() { - continue; - }; - - // sanity check - ensure!( - bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), - ContractError::InvalidShare - ); - - for reward in reward_bucket.total.iter() { - let user_reward = reward.amount * bonding_weight_response_for_epoch.share; - - // make sure the reward is sound - let reward_validation: Result<(), StdError> = reward_bucket - .available - .iter() - .find(|available_fee| available_fee.denom == reward.denom) - .map(|available_fee| { - if user_reward > available_fee.amount { - attributes.push(( - "error", - ContractError::InvalidReward { - reward: user_reward, - available: available_fee.amount, - } - .to_string(), - )); - } - Ok(()) - }) - .ok_or(StdError::generic_err("Invalid fee"))?; - - // if the reward is invalid, skip the bucket - match reward_validation { - Ok(_) => {} - Err(_) => continue, - } - - let denom = &reward.denom; - // add the reward - claimable_rewards = asset::aggregate_coins( - claimable_rewards, - vec![Coin { - denom: denom.to_string(), - amount: user_reward, - }], - )?; - - // modify the bucket to reflect the new available and claimed amount - for available_fee in reward_bucket.available.iter_mut() { - if available_fee.denom == reward.denom { - available_fee.amount = available_fee.amount.saturating_sub(user_reward); - } - } - - if reward_bucket.claimed.is_empty() { - reward_bucket.claimed = vec![Coin { - denom: denom.to_string(), - amount: user_reward, - }]; - } else { - for claimed_reward in reward_bucket.claimed.iter_mut() { - if claimed_reward.denom == reward.denom { - claimed_reward.amount = claimed_reward.amount.checked_add(user_reward)?; - } - - // sanity check, should never happen - for total_reward in reward_bucket.total.iter() { - if total_reward.denom == claimed_reward.denom { - ensure!( - claimed_reward.amount <= total_reward.amount, - ContractError::InvalidShare - ); - } - } - } - } - - REWARD_BUCKETS.save(deps.storage, reward_bucket.id, &reward_bucket)?; - } - } - - // update the last claimed epoch for the user. it's in the first bucket on the list since it's sorted - // in descending order - LAST_CLAIMED_EPOCH.save( - deps.storage, - &info.sender, - &claimable_reward_buckets_for_user[0].id, - )?; - - Ok(Response::default() - .add_attributes(vec![("action", "claim".to_string())]) - .add_attributes(attributes) - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: claimable_rewards, - }))) -} - -pub(crate) fn fill_rewards( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - println!("----fill_rewards----"); - - // Finding the most recent bucket - let upcoming_bucket_id = match REWARD_BUCKETS - .keys(deps.storage, None, None, Order::Descending) - .next() - { - Some(bucket_id) => bucket_id?, - None => return Err(ContractError::Unauthorized), - }; - - let config = CONFIG.load(deps.storage)?; - let distribution_denom = config.distribution_denom.clone(); - - let mut messages: Vec = vec![]; - let mut submessages: Vec = vec![]; - // swap non-whale to whale - // Search info funds for LP tokens, LP tokens will contain LP_SYMBOL from lp_common and the string .pair. - let mut whale = info - .funds - .iter() - .find(|coin| coin.denom.eq(distribution_denom.as_str())) - .unwrap_or(&Coin { - denom: distribution_denom.clone(), - amount: Uint128::zero(), - }) - .to_owned(); - - // coins (not the distribution_denom) that are laying in the contract and have not been swapped before for lack - // of swap routes - let remanent_coins = deps - .querier - .query_all_balances(env.contract.address)? - .into_iter() - .filter(|coin| coin.denom.ne(distribution_denom.as_str())) - .collect::>(); - - // Each of these helpers will add messages to the messages vector - // and may increment the whale Coin above with the result of the swaps - helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; - helpers::swap_coins_to_main_token( - remanent_coins, - &deps, - config, - &mut whale, - &distribution_denom, - &mut messages, - )?; - - // Add the whale to the funds, the whale figure now should be the result - // of all the LP token withdrawals and swaps - // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned - // If this became an issue would could look at replys instead of the query - REWARD_BUCKETS.update(deps.storage, upcoming_bucket_id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - })?; - Ok(Response::default() - .add_messages(messages) - .add_submessages(submessages) - .add_attributes(vec![("action", "fill_rewards".to_string())])) -} - -pub(crate) fn on_epoch_created( - deps: DepsMut, - info: MessageInfo, - current_epoch: Epoch, -) -> Result { - cw_utils::nonpayable(&info)?; - - println!("----on_epoch_created----"); - 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!( - info.sender == config.epoch_manager_addr, - ContractError::Unauthorized - ); - - let global = GLOBAL.may_load(deps.storage)?; - // This happens only on the very first epoch where Global has not been initialised yet - if global.is_none() { - let initial_global_index = GlobalIndex { - updated_last: current_epoch.id, - ..Default::default() - }; - GLOBAL.save(deps.storage, &initial_global_index)?; - REWARD_BUCKETS.save( - deps.storage, - current_epoch.id, - &RewardBucket { - id: current_epoch.id, - epoch_start_time: current_epoch.start_time, - global_index: initial_global_index, - ..RewardBucket::default() - }, - )?; - } - - // Update the global index epoch id field - let mut global = GLOBAL.load(deps.storage)?; - global.epoch_id = current_epoch.id; - - // update the global index for the current bucket, take the current snapshot of the global index - REWARD_BUCKETS.update( - deps.storage, - current_epoch.id, - |reward_bucket| -> StdResult<_> { - let mut reward_bucket = reward_bucket.unwrap_or_default(); - reward_bucket.global_index = global; - Ok(reward_bucket) - }, - )?; - - // forward fees from the expiring bucket to the new one. - let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; - if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { - // Load all the available assets from the expiring bucket - let amount_to_be_forwarded = REWARD_BUCKETS - .load(deps.storage, expiring_bucket.id)? - .available; - REWARD_BUCKETS.update(deps.storage, current_epoch.id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = - asset::aggregate_coins(bucket.available, amount_to_be_forwarded.clone())?; - bucket.total = asset::aggregate_coins(bucket.total, amount_to_be_forwarded)?; - - Ok(bucket) - })?; - // Set the available assets for the expiring bucket to an empty vec now that they have been - // forwarded - REWARD_BUCKETS.update(deps.storage, expiring_bucket.id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = vec![]; - Ok(bucket) - })?; - } - - // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in - // the next epoch. Also, forwards the expiring bucket (only 21 bucket are live at a given moment) - let next_epoch_id = Uint64::new(current_epoch.id) - .checked_add(Uint64::one())? - .u64(); - REWARD_BUCKETS.save( - deps.storage, - next_epoch_id, - &RewardBucket { - id: next_epoch_id, - epoch_start_time: current_epoch.start_time.plus_days(1), - // this global index is to be updated the next time this hook is called, as this future epoch - // will become the current one - global_index: Default::default(), - ..RewardBucket::default() - }, - )?; - - Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) -} diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index ecb27431..c6e0fe19 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -9,7 +9,7 @@ use white_whale_std::pool_network::asset; use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, REWARD_BUCKETS}; -use crate::{commands, queries}; +use crate::{bonding, commands, queries, rewards}; // version info for migration info const CONTRACT_NAME: &str = "white_whale-bonding_manager"; @@ -65,15 +65,15 @@ pub fn execute( match msg { ExecuteMsg::Bond => { let asset_to_bond = helpers::validate_funds(&deps, &info)?; - commands::bond(deps, info, env, asset_to_bond) + bonding::commands::bond(deps, info, env, asset_to_bond) } ExecuteMsg::Unbond { asset } => { cw_utils::nonpayable(&info)?; - commands::unbond(deps, info, env, asset) + bonding::commands::unbond(deps, info, env, asset) } ExecuteMsg::Withdraw { denom } => { cw_utils::nonpayable(&info)?; - commands::withdraw(deps, info.sender, denom) + bonding::commands::withdraw(deps, info.sender, denom) } ExecuteMsg::UpdateConfig { epoch_manager_addr, @@ -91,10 +91,10 @@ pub fn execute( growth_rate, ) } - ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), - ExecuteMsg::Claim => commands::claim(deps, info), + ExecuteMsg::FillRewards => rewards::commands::fill_rewards(deps, env, info), + ExecuteMsg::Claim => rewards::commands::claim(deps, info), ExecuteMsg::EpochChangedHook { current_epoch } => { - commands::on_epoch_created(deps, info, current_epoch) + rewards::commands::on_epoch_created(deps, info, current_epoch) } ExecuteMsg::UpdateOwnership(action) => { cw_utils::nonpayable(&info)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/distribution/mod.rs b/contracts/liquidity_hub/bonding-manager/src/distribution/mod.rs deleted file mode 100644 index 82b6da3c..00000000 --- a/contracts/liquidity_hub/bonding-manager/src/distribution/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod commands; diff --git a/contracts/liquidity_hub/bonding-manager/src/lib.rs b/contracts/liquidity_hub/bonding-manager/src/lib.rs index 206bb543..bcd332bb 100644 --- a/contracts/liquidity_hub/bonding-manager/src/lib.rs +++ b/contracts/liquidity_hub/bonding-manager/src/lib.rs @@ -1,8 +1,10 @@ +mod bonding; mod commands; pub mod contract; mod error; pub mod helpers; mod queries; +mod rewards; pub mod state; #[cfg(test)] diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs new file mode 100644 index 00000000..1134243b --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -0,0 +1,296 @@ +use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight}; +use crate::state::{CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS}; +use crate::{helpers, ContractError}; +use cosmwasm_std::{ + ensure, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, + StdError, StdResult, SubMsg, Uint128, Uint64, +}; +use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket}; +use white_whale_std::epoch_manager::epoch_manager::Epoch; +use white_whale_std::pool_network::asset; + +pub(crate) fn fill_rewards( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + println!("----fill_rewards----"); + + // Finding the most recent bucket + let upcoming_bucket_id = match REWARD_BUCKETS + .keys(deps.storage, None, None, Order::Descending) + .next() + { + Some(bucket_id) => bucket_id?, + None => return Err(ContractError::Unauthorized), + }; + + let config = CONFIG.load(deps.storage)?; + let distribution_denom = config.distribution_denom.clone(); + + let mut messages: Vec = vec![]; + let mut submessages: Vec = vec![]; + // swap non-whale to whale + // Search info funds for LP tokens, LP tokens will contain LP_SYMBOL from lp_common and the string .pair. + let mut whale = info + .funds + .iter() + .find(|coin| coin.denom.eq(distribution_denom.as_str())) + .unwrap_or(&Coin { + denom: distribution_denom.clone(), + amount: Uint128::zero(), + }) + .to_owned(); + + // coins (not the distribution_denom) that are laying in the contract and have not been swapped before for lack + // of swap routes + let remanent_coins = deps + .querier + .query_all_balances(env.contract.address)? + .into_iter() + .filter(|coin| coin.denom.ne(distribution_denom.as_str())) + .collect::>(); + + // Each of these helpers will add messages to the messages vector + // and may increment the whale Coin above with the result of the swaps + helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; + helpers::swap_coins_to_main_token( + remanent_coins, + &deps, + config, + &mut whale, + &distribution_denom, + &mut messages, + )?; + + // Add the whale to the funds, the whale figure now should be the result + // of all the LP token withdrawals and swaps + // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned + // If this became an issue would could look at replys instead of the query + REWARD_BUCKETS.update(deps.storage, upcoming_bucket_id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; + bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; + Ok(bucket) + })?; + Ok(Response::default() + .add_messages(messages) + .add_submessages(submessages) + .add_attributes(vec![("action", "fill_rewards".to_string())])) +} + +pub(crate) fn on_epoch_created( + deps: DepsMut, + info: MessageInfo, + current_epoch: Epoch, +) -> Result { + cw_utils::nonpayable(&info)?; + + println!("----on_epoch_created----"); + 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!( + info.sender == config.epoch_manager_addr, + ContractError::Unauthorized + ); + + let global = GLOBAL.may_load(deps.storage)?; + // This happens only on the very first epoch where Global has not been initialised yet + if global.is_none() { + let initial_global_index = GlobalIndex { + updated_last: current_epoch.id, + ..Default::default() + }; + GLOBAL.save(deps.storage, &initial_global_index)?; + REWARD_BUCKETS.save( + deps.storage, + current_epoch.id, + &RewardBucket { + id: current_epoch.id, + epoch_start_time: current_epoch.start_time, + global_index: initial_global_index, + ..RewardBucket::default() + }, + )?; + } + + // Update the global index epoch id field + let mut global = GLOBAL.load(deps.storage)?; + global.epoch_id = current_epoch.id; + + // update the global index for the current bucket, take the current snapshot of the global index + REWARD_BUCKETS.update( + deps.storage, + current_epoch.id, + |reward_bucket| -> StdResult<_> { + let mut reward_bucket = reward_bucket.unwrap_or_default(); + reward_bucket.global_index = global; + Ok(reward_bucket) + }, + )?; + + // forward fees from the expiring bucket to the new one. + let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; + if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { + // Load all the available assets from the expiring bucket + let amount_to_be_forwarded = REWARD_BUCKETS + .load(deps.storage, expiring_bucket.id)? + .available; + REWARD_BUCKETS.update(deps.storage, current_epoch.id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = + asset::aggregate_coins(bucket.available, amount_to_be_forwarded.clone())?; + bucket.total = asset::aggregate_coins(bucket.total, amount_to_be_forwarded)?; + + Ok(bucket) + })?; + // Set the available assets for the expiring bucket to an empty vec now that they have been + // forwarded + REWARD_BUCKETS.update(deps.storage, expiring_bucket.id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = vec![]; + Ok(bucket) + })?; + } + + // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in + // the next epoch. Also, forwards the expiring bucket (only 21 bucket are live at a given moment) + let next_epoch_id = Uint64::new(current_epoch.id) + .checked_add(Uint64::one())? + .u64(); + REWARD_BUCKETS.save( + deps.storage, + next_epoch_id, + &RewardBucket { + id: next_epoch_id, + epoch_start_time: current_epoch.start_time.plus_days(1), + // this global index is to be updated the next time this hook is called, as this future epoch + // will become the current one + global_index: Default::default(), + ..RewardBucket::default() + }, + )?; + + Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) +} + +/// Claims pending rewards for the sender. +pub fn claim(deps: DepsMut, info: MessageInfo) -> Result { + let claimable_reward_buckets_for_user = + query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.reward_buckets; + ensure!( + !claimable_reward_buckets_for_user.is_empty(), + ContractError::NothingToClaim + ); + + let mut claimable_rewards = vec![]; + let mut attributes = vec![]; + for mut reward_bucket in claimable_reward_buckets_for_user.clone() { + let bonding_weight_response_for_epoch = query_weight( + deps.as_ref(), + reward_bucket.id, + info.sender.to_string(), + Some(reward_bucket.global_index.clone()), + )?; + + // if the user has no share in the bucket, skip it + if bonding_weight_response_for_epoch.share.is_zero() { + continue; + }; + + // sanity check + ensure!( + bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), + ContractError::InvalidShare + ); + + for reward in reward_bucket.total.iter() { + let user_reward = reward.amount * bonding_weight_response_for_epoch.share; + + // make sure the reward is sound + let reward_validation: Result<(), StdError> = reward_bucket + .available + .iter() + .find(|available_fee| available_fee.denom == reward.denom) + .map(|available_fee| { + if user_reward > available_fee.amount { + attributes.push(( + "error", + ContractError::InvalidReward { + reward: user_reward, + available: available_fee.amount, + } + .to_string(), + )); + } + Ok(()) + }) + .ok_or(StdError::generic_err("Invalid fee"))?; + + // if the reward is invalid, skip the bucket + match reward_validation { + Ok(_) => {} + Err(_) => continue, + } + + let denom = &reward.denom; + // add the reward + claimable_rewards = asset::aggregate_coins( + claimable_rewards, + vec![Coin { + denom: denom.to_string(), + amount: user_reward, + }], + )?; + + // modify the bucket to reflect the new available and claimed amount + for available_fee in reward_bucket.available.iter_mut() { + if available_fee.denom == reward.denom { + available_fee.amount = available_fee.amount.saturating_sub(user_reward); + } + } + + if reward_bucket.claimed.is_empty() { + reward_bucket.claimed = vec![Coin { + denom: denom.to_string(), + amount: user_reward, + }]; + } else { + for claimed_reward in reward_bucket.claimed.iter_mut() { + if claimed_reward.denom == reward.denom { + claimed_reward.amount = claimed_reward.amount.checked_add(user_reward)?; + } + + // sanity check, should never happen + for total_reward in reward_bucket.total.iter() { + if total_reward.denom == claimed_reward.denom { + ensure!( + claimed_reward.amount <= total_reward.amount, + ContractError::InvalidShare + ); + } + } + } + } + + REWARD_BUCKETS.save(deps.storage, reward_bucket.id, &reward_bucket)?; + } + } + + // update the last claimed epoch for the user. it's in the first bucket on the list since it's sorted + // in descending order + LAST_CLAIMED_EPOCH.save( + deps.storage, + &info.sender, + &claimable_reward_buckets_for_user[0].id, + )?; + + Ok(Response::default() + .add_attributes(vec![("action", "claim".to_string())]) + .add_attributes(attributes) + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: claimable_rewards, + }))) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/collection/mod.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/mod.rs similarity index 100% rename from contracts/liquidity_hub/bonding-manager/src/collection/mod.rs rename to contracts/liquidity_hub/bonding-manager/src/rewards/mod.rs diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 72705f79..5c6a613b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -13,6 +13,11 @@ pub const GLOBAL: Item = Item::new("global"); pub const LAST_CLAIMED_EPOCH: Map<&Addr, u64> = Map::new("last_claimed_epoch"); pub const REWARD_BUCKETS: Map = Map::new("reward_buckets"); +/// This is the upcoming reward bucket that will hold the rewards coming to the contract after a +/// new epoch gets created. Once a new epoch is created, this bucket will be forwarded to the +/// reward buckets map, and reset for the new rewards to come. +pub const UPCOMING_REWARD_BUCKET: Item = Item::new("upcoming_reward_bucket"); + /// Updates the local weight of the given address. pub fn update_bond_weight( deps: &mut DepsMut, From f1b18fbb2fa429ff53a156a3ae40fbd9e04e1257 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 13 May 2024 15:56:56 +0100 Subject: [PATCH 28/51] refactor: using upcoming_reward_bucket when filling rewards --- .../bonding-manager/src/bonding/commands.rs | 2 +- .../bonding-manager/src/contract.rs | 103 ++------ .../bonding-manager/src/helpers.rs | 21 +- .../bonding-manager/src/queries.rs | 12 +- .../bonding-manager/src/rewards/commands.rs | 249 ++++++++++-------- .../bonding-manager/src/state.rs | 19 +- .../white-whale-std/src/bonding_manager.rs | 7 + 7 files changed, 190 insertions(+), 223 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index 5de2f3b3..481a10e6 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -16,7 +16,7 @@ pub(crate) fn bond( asset: Coin, ) -> Result { println!("----bond----"); - helpers::validate_buckets(&deps)?; + helpers::validate_buckets_not_empty(&deps)?; helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index c6e0fe19..6077fce5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -1,15 +1,13 @@ -use cosmwasm_std::{ensure, entry_point, from_json, Addr, Coin, Order, Reply, Uint128}; -use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; -use cw2::{get_contract_version, set_contract_version}; -use cw_utils::parse_reply_execute_data; - -use white_whale_std::bonding_manager::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use white_whale_std::pool_network::asset; - use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; -use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, REWARD_BUCKETS}; +use crate::state::{BONDING_ASSETS_LIMIT, 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, +}; // version info for migration info const CONTRACT_NAME: &str = "white_whale-bonding_manager"; @@ -45,6 +43,9 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + // Initialize the upcoming reward bucket + UPCOMING_REWARD_BUCKET.save(deps.storage, &UpcomingRewardBucket::default())?; + Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), ("owner", info.sender.to_string()), @@ -55,6 +56,15 @@ pub fn instantiate( ])) } +// Reply entrypoint handling LP withdraws from fill_rewards +#[entry_point] +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), + _ => Err(ContractError::Unauthorized), + } +} + #[entry_point] pub fn execute( deps: DepsMut, @@ -158,81 +168,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Result { - match msg.id { - LP_WITHDRAWAL_REPLY_ID => { - // Read the epoch sent by the fee collector through the ForwardFeesResponse - let execute_contract_response = parse_reply_execute_data(msg.clone()).unwrap(); - let data = execute_contract_response - .data - .ok_or(ContractError::Unauthorized)?; - - let coins: Vec = from_json(data.as_slice())?; - let config = CONFIG.load(deps.storage)?; - let distribution_denom = config.distribution_denom.clone(); - let mut messages = vec![]; - - // Search received coins funds for the coin that is not the distribution denom - // This will be swapped for - let mut to_be_distribution_asset = coins - .iter() - .find(|coin| coin.denom.ne(distribution_denom.as_str())) - .unwrap_or(&Coin { - denom: config.distribution_denom.clone(), - amount: Uint128::zero(), - }) - .to_owned(); - println!("reply"); - // Swap other coins to the distribution denom - helpers::swap_coins_to_main_token( - coins, - &deps, - config, - &mut to_be_distribution_asset, - &distribution_denom, - &mut messages, - )?; - - // if the swap was successful and the to_be_distribution_asset.denom is the - // distribution_denom, update the upcoming epoch with the new funds - if to_be_distribution_asset.denom == distribution_denom { - // Finding the upcoming EpochID - let upcoming_epoch_id = match REWARD_BUCKETS - .keys(deps.storage, None, None, Order::Descending) - .next() - { - Some(epoch_id) => epoch_id?, - None => return Err(ContractError::Unauthorized), - }; - - REWARD_BUCKETS.update( - deps.storage, - upcoming_epoch_id, - |epoch| -> StdResult<_> { - let mut upcoming_epoch = epoch.unwrap_or_default(); - upcoming_epoch.available = asset::aggregate_coins( - upcoming_epoch.available, - vec![to_be_distribution_asset.clone()], - )?; - upcoming_epoch.total = asset::aggregate_coins( - upcoming_epoch.total, - vec![to_be_distribution_asset.clone()], - )?; - Ok(upcoming_epoch) - }, - )?; - } - - Ok(Response::new() - .add_messages(messages) - .add_attribute("total_withdrawn", msg.id.to_string())) - } - _ => Err(ContractError::Unauthorized), - } -} - #[cfg(not(tarpaulin_include))] #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 16af187e..07b452d7 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{ StdResult, SubMsg, WasmMsg, }; use cw_utils::PaymentError; + use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, Config}; use white_whale_std::constants::LP_SYMBOL; use white_whale_std::epoch_manager::epoch_manager::EpochResponse; @@ -84,7 +85,7 @@ pub fn validate_bonding_for_current_epoch(deps: &DepsMut) -> Result<(), Contract // Used in FillRewards to search the funds for LP tokens and withdraw them // If we do get some LP tokens to withdraw they could be swapped to whale in the reply -pub fn handle_lp_tokens( +pub fn handle_lp_tokens_rewards( funds: &Vec, config: &Config, submessages: &mut Vec, @@ -163,16 +164,14 @@ pub fn swap_coins_to_main_token( for coin in coins_to_swap { println!("Swapping {} to {}", coin.denom, distribution_denom); - let swap_route_query = white_whale_std::pool_manager::QueryMsg::SwapRoute { - offer_asset_denom: coin.denom.to_string(), - ask_asset_denom: distribution_denom.to_string(), - }; - - println!("he"); // Query for the routes and pool - let swap_routes_response: StdResult = deps - .querier - .query_wasm_smart(config.pool_manager_addr.to_string(), &swap_route_query); + let swap_routes_response: StdResult = deps.querier.query_wasm_smart( + config.pool_manager_addr.to_string(), + &white_whale_std::pool_manager::QueryMsg::SwapRoute { + offer_asset_denom: coin.denom.to_string(), + ask_asset_denom: distribution_denom.to_string(), + }, + ); println!("swap_routes_response: {:?}", swap_routes_response); let swap_routes = match swap_routes_response { @@ -247,7 +246,7 @@ pub fn swap_coins_to_main_token( /// Validates that there are reward buckets in the state. If there are none, it means the system has just /// been started and the epoch manager has still not created any epochs yet. -pub(crate) fn validate_buckets(deps: &DepsMut) -> Result<(), ContractError> { +pub(crate) fn validate_buckets_not_empty(deps: &DepsMut) -> Result<(), ContractError> { let reward_buckets = REWARD_BUCKETS .keys(deps.storage, None, None, Order::Descending) .collect::>>()?; diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 2c8b1c01..685ba130 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -266,14 +266,12 @@ pub fn query_global_index(deps: Deps, epoch_id: Option) -> StdResult Result, ContractError> { let config = CONFIG.load(deps.storage)?; - // Adding 1 because we store the future bucket in the map also, so grace_period + 1 - let grace_period_plus_future_bucket = Uint64::new(config.grace_period) - .checked_add(Uint64::one())? - .u64(); - // Take grace_period + 1 and then slice last one off + let grace_period = config.grace_period; + + // Take grace_period let buckets = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) - .take(grace_period_plus_future_bucket as usize) + .take(grace_period as usize) .map(|item| { let (_, bucket) = item?; Ok(bucket) @@ -282,7 +280,7 @@ pub fn get_expiring_reward_bucket(deps: Deps) -> Result, Co // if the buckets vector's length is the same as the grace period it means there is one bucket that // is expiring once the new one is created i.e. the last bucket in the vector - if buckets.len() == grace_period_plus_future_bucket as usize { + if buckets.len() == grace_period as usize { let expiring_reward_bucket: RewardBucket = buckets.into_iter().last().unwrap_or_default(); Ok(Some(expiring_reward_bucket)) } else { diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs index 1134243b..d5227dcf 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -1,14 +1,92 @@ -use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight}; -use crate::state::{CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS}; -use crate::{helpers, ContractError}; use cosmwasm_std::{ - ensure, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, - StdError, StdResult, SubMsg, Uint128, Uint64, + ensure, from_json, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Reply, + Response, StdError, SubMsg, Uint128, }; -use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket}; +use cw_utils::parse_reply_execute_data; + +use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket, UpcomingRewardBucket}; use white_whale_std::epoch_manager::epoch_manager::Epoch; use white_whale_std::pool_network::asset; +use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight}; +use crate::state::{ + fill_upcoming_reward_bucket, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, + UPCOMING_REWARD_BUCKET, +}; +use crate::{helpers, ContractError}; + +/// Handles the new epoch created by the epoch manager. It creates a new reward bucket with the +/// fees that have been accrued in the previous epoch, creates a new bucket for the upcoming rewards +/// and forwards the fees from the expiring bucket to the new one. +pub(crate) fn on_epoch_created( + deps: DepsMut, + info: MessageInfo, + current_epoch: Epoch, +) -> Result { + cw_utils::nonpayable(&info)?; + + println!("----on_epoch_created----"); + 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!( + info.sender == config.epoch_manager_addr, + ContractError::Unauthorized + ); + + let global = GLOBAL.may_load(deps.storage)?; + // This happens only on the very first epoch where Global has not been initialised yet + if global.is_none() { + let initial_global_index = GlobalIndex { + epoch_id: current_epoch.id, + updated_last: current_epoch.id, + ..Default::default() + }; + GLOBAL.save(deps.storage, &initial_global_index)?; + } + + // Update the global index epoch id + let mut global_index = GLOBAL.load(deps.storage)?; + global_index.epoch_id = current_epoch.id; + + // 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)?; + let mut new_reward_bucket = RewardBucket { + id: current_epoch.id, + epoch_start_time: current_epoch.start_time, + total: upcoming_bucket.total.clone(), + available: upcoming_bucket.total, + claimed: vec![], + global_index, + }; + + // forward fees from the expiring bucket to the new one. + let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; + if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { + // Aggregate the available assets from the expiring bucket to the new reward bucket + new_reward_bucket.available = asset::aggregate_coins( + new_reward_bucket.available, + expiring_bucket.available.clone(), + )?; + new_reward_bucket.total = + asset::aggregate_coins(new_reward_bucket.total, expiring_bucket.available.clone())?; + + // Set the available assets for the expiring bucket to an empty vec now that they have been + // forwarded + expiring_bucket.available.clear(); + REWARD_BUCKETS.save(deps.storage, expiring_bucket.id, expiring_bucket)?; + } + + // Save the new reward bucket + REWARD_BUCKETS.save(deps.storage, current_epoch.id, &new_reward_bucket)?; + // Reset the upcoming bucket + UPCOMING_REWARD_BUCKET.save(deps.storage, &UpcomingRewardBucket::default())?; + + Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) +} + +/// Fills the upcoming rewards bucket with the upcoming fees. pub(crate) fn fill_rewards( deps: DepsMut, env: Env, @@ -16,23 +94,14 @@ pub(crate) fn fill_rewards( ) -> Result { println!("----fill_rewards----"); - // Finding the most recent bucket - let upcoming_bucket_id = match REWARD_BUCKETS - .keys(deps.storage, None, None, Order::Descending) - .next() - { - Some(bucket_id) => bucket_id?, - None => return Err(ContractError::Unauthorized), - }; - let config = CONFIG.load(deps.storage)?; let distribution_denom = config.distribution_denom.clone(); let mut messages: Vec = vec![]; let mut submessages: Vec = vec![]; // swap non-whale to whale - // Search info funds for LP tokens, LP tokens will contain LP_SYMBOL from lp_common and the string .pair. - let mut whale = info + + let mut distribution_denom_in_tx = info .funds .iter() .find(|coin| coin.denom.eq(distribution_denom.as_str())) @@ -44,7 +113,7 @@ pub(crate) fn fill_rewards( // coins (not the distribution_denom) that are laying in the contract and have not been swapped before for lack // of swap routes - let remanent_coins = deps + let remnant_coins = deps .querier .query_all_balances(env.contract.address)? .into_iter() @@ -52,127 +121,73 @@ pub(crate) fn fill_rewards( .collect::>(); // Each of these helpers will add messages to the messages vector - // and may increment the whale Coin above with the result of the swaps - helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; + // and may increment the distribution_denom Coin above with the result of the swaps + helpers::handle_lp_tokens_rewards(&remnant_coins, &config, &mut submessages)?; helpers::swap_coins_to_main_token( - remanent_coins, + remnant_coins, &deps, config, - &mut whale, + &mut distribution_denom_in_tx, &distribution_denom, &mut messages, )?; // Add the whale to the funds, the whale figure now should be the result - // of all the LP token withdrawals and swaps + // of all the swaps // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned - // If this became an issue would could look at replys instead of the query - REWARD_BUCKETS.update(deps.storage, upcoming_bucket_id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - })?; + // If this became an issue we could look at replies instead of the query + // The lp_tokens being withdrawn are handled in the reply entry point + fill_upcoming_reward_bucket(deps, distribution_denom_in_tx.clone())?; + Ok(Response::default() .add_messages(messages) .add_submessages(submessages) .add_attributes(vec![("action", "fill_rewards".to_string())])) } -pub(crate) fn on_epoch_created( - deps: DepsMut, - info: MessageInfo, - current_epoch: Epoch, -) -> Result { - cw_utils::nonpayable(&info)?; +/// 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---"); - println!("----on_epoch_created----"); - 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!( - info.sender == config.epoch_manager_addr, - ContractError::Unauthorized - ); + // 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 + .data + .ok_or(ContractError::Unauthorized)?; - let global = GLOBAL.may_load(deps.storage)?; - // This happens only on the very first epoch where Global has not been initialised yet - if global.is_none() { - let initial_global_index = GlobalIndex { - updated_last: current_epoch.id, - ..Default::default() - }; - GLOBAL.save(deps.storage, &initial_global_index)?; - REWARD_BUCKETS.save( - deps.storage, - current_epoch.id, - &RewardBucket { - id: current_epoch.id, - epoch_start_time: current_epoch.start_time, - global_index: initial_global_index, - ..RewardBucket::default() - }, - )?; - } + let coins: Vec = from_json(data.as_slice())?; + let config = CONFIG.load(deps.storage)?; + let distribution_denom = config.distribution_denom.clone(); + let mut messages = vec![]; - // Update the global index epoch id field - let mut global = GLOBAL.load(deps.storage)?; - global.epoch_id = current_epoch.id; + // Search received coins funds for the coin that is not the distribution denom + // This will be swapped for + let mut distribution_asset = coins + .iter() + .find(|coin| coin.denom.eq(distribution_denom.as_str())) + .unwrap_or(&Coin { + denom: distribution_denom.clone(), + amount: Uint128::zero(), + }) + .to_owned(); - // update the global index for the current bucket, take the current snapshot of the global index - REWARD_BUCKETS.update( - deps.storage, - current_epoch.id, - |reward_bucket| -> StdResult<_> { - let mut reward_bucket = reward_bucket.unwrap_or_default(); - reward_bucket.global_index = global; - Ok(reward_bucket) - }, + // Swap other coins to the distribution denom + helpers::swap_coins_to_main_token( + coins, + &deps, + config, + &mut distribution_asset, + &distribution_denom, + &mut messages, )?; - // forward fees from the expiring bucket to the new one. - let mut expiring_reward_bucket = get_expiring_reward_bucket(deps.as_ref())?; - if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { - // Load all the available assets from the expiring bucket - let amount_to_be_forwarded = REWARD_BUCKETS - .load(deps.storage, expiring_bucket.id)? - .available; - REWARD_BUCKETS.update(deps.storage, current_epoch.id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = - asset::aggregate_coins(bucket.available, amount_to_be_forwarded.clone())?; - bucket.total = asset::aggregate_coins(bucket.total, amount_to_be_forwarded)?; - - Ok(bucket) - })?; - // Set the available assets for the expiring bucket to an empty vec now that they have been - // forwarded - REWARD_BUCKETS.update(deps.storage, expiring_bucket.id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = vec![]; - Ok(bucket) - })?; - } - - // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in - // the next epoch. Also, forwards the expiring bucket (only 21 bucket are live at a given moment) - let next_epoch_id = Uint64::new(current_epoch.id) - .checked_add(Uint64::one())? - .u64(); - REWARD_BUCKETS.save( - deps.storage, - next_epoch_id, - &RewardBucket { - id: next_epoch_id, - epoch_start_time: current_epoch.start_time.plus_days(1), - // this global index is to be updated the next time this hook is called, as this future epoch - // will become the current one - global_index: Default::default(), - ..RewardBucket::default() - }, - )?; + // update the upcoming bucket with the new funds + fill_upcoming_reward_bucket(deps, distribution_asset.clone())?; - Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) + Ok(Response::new() + .add_messages(messages) + .add_attribute("total_withdrawn", msg.id.to_string())) } /// Claims pending rewards for the sender. diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 5c6a613b..5fa419d8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,7 +1,10 @@ use crate::ContractError; -use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Addr, Coin, Decimal, Deps, DepsMut, Order, StdError, StdResult, Uint128}; use cw_storage_plus::{Item, Map}; -use white_whale_std::bonding_manager::{Bond, Config, GlobalIndex, RewardBucket}; +use white_whale_std::bonding_manager::{ + Bond, Config, GlobalIndex, RewardBucket, UpcomingRewardBucket, +}; +use white_whale_std::pool_network::asset; type Denom = str; @@ -16,7 +19,7 @@ pub const REWARD_BUCKETS: Map = Map::new("reward_buckets"); /// This is the upcoming reward bucket that will hold the rewards coming to the contract after a /// new epoch gets created. Once a new epoch is created, this bucket will be forwarded to the /// reward buckets map, and reset for the new rewards to come. -pub const UPCOMING_REWARD_BUCKET: Item = Item::new("upcoming_reward_bucket"); +pub const UPCOMING_REWARD_BUCKET: Item = Item::new("upcoming_reward_bucket"); /// Updates the local weight of the given address. pub fn update_bond_weight( @@ -112,3 +115,13 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { Ok(None) } } + +/// Fills the upcoming reward bucket with the given funds. +pub fn fill_upcoming_reward_bucket(deps: DepsMut, funds: Coin) -> StdResult<()> { + UPCOMING_REWARD_BUCKET.update(deps.storage, |mut upcoming_bucket| -> StdResult<_> { + upcoming_bucket.total = asset::aggregate_coins(upcoming_bucket.total, vec![funds])?; + Ok(upcoming_bucket) + })?; + + Ok(()) +} diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index faa0e883..07fd3144 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -45,6 +45,13 @@ pub struct RewardBucket { pub global_index: GlobalIndex, } +#[cw_serde] +#[derive(Default)] +pub struct UpcomingRewardBucket { + // The fees to be distributed in this reward bucket. + pub total: Vec, +} + #[cw_serde] pub struct Bond { /// The amount of bonded tokens. From 5de11efd6b0f7381865e038c19dd27fb4355e304 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 14 May 2024 15:02:05 +0100 Subject: [PATCH 29/51] chore: add last_claimed_epoch when bonding --- .../bonding-manager/src/bonding/commands.rs | 12 ++++- .../bonding-manager/src/helpers.rs | 13 +++++ .../bonding-manager/src/queries.rs | 52 +++++-------------- .../bonding-manager/src/rewards/commands.rs | 3 +- .../bonding-manager/src/tests/bond.rs | 8 +-- .../white-whale-std/src/bonding_manager.rs | 3 +- 6 files changed, 45 insertions(+), 46 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index 481a10e6..8fc313b8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -1,5 +1,7 @@ use crate::queries::MAX_PAGE_LIMIT; -use crate::state::{update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, UNBOND}; +use crate::state::{ + update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, +}; use crate::{helpers, ContractError}; use cosmwasm_std::{ ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, @@ -17,6 +19,7 @@ pub(crate) fn bond( ) -> 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)?; @@ -40,11 +43,17 @@ pub(crate) fn bond( ..Bond::default() }); + // if bond.updated_last != current_epoch.epoch.id.clone() { + // bond.previous = Some((bond.updated_last, bond.weight)); + // } + // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; + LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.epoch.id)?; + // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); // include time term in the weight @@ -119,6 +128,7 @@ pub(crate) fn unbond( weight: Uint128::zero(), updated_last: current_epoch.epoch.id, created_at_epoch: current_epoch.epoch.id, + //previous: None, }, )?; // update global values diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 07b452d7..b4695e41 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -86,6 +86,7 @@ pub fn validate_bonding_for_current_epoch(deps: &DepsMut) -> Result<(), Contract // Used in FillRewards to search the funds for LP tokens and withdraw them // 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, config: &Config, submessages: &mut Vec, @@ -104,6 +105,18 @@ pub fn handle_lp_tokens_rewards( 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(), + &white_whale_std::pool_manager::QueryMsg::Pool { + pool_identifier: pool_identifier.to_string(), + }, + ); + + if pool.is_err() { + continue; + } + // if LP Tokens ,verify and withdraw then swap to whale let lp_withdrawal_msg = white_whale_std::pool_manager::ExecuteMsg::WithdrawLiquidity { pool_identifier: pool_identifier.to_string(), diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 685ba130..249f5603 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::{HashSet, VecDeque}; -use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Uint128, Uint64}; +use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Uint128}; use cw_storage_plus::Bound; use crate::ContractError; @@ -179,14 +179,16 @@ pub(crate) fn query_weight( for (_, mut bond) in bonds? { println!("bond-before: {:?}", bond); - // if bond.updated_last == current_epoch.epoch.id { - // // take previous value - // } - - //todo - if bond.created_at_epoch == current_epoch.epoch.id { + if bond.updated_last == current_epoch.epoch.id { continue; } + // + // println!("bond.previous.is_some(): {:?} ", bond.previous.is_some() && bond.previous.unwrap().0 == current_epoch.epoch.id); + // //todo + // if bond.created_at_epoch == current_epoch.epoch.id || + // bond.previous.is_some() && bond.previous.unwrap().0 == current_epoch.epoch.id { + // continue; + // } bond.weight = get_weight( epoch_id, @@ -293,11 +295,8 @@ pub fn get_expiring_reward_bucket(deps: Deps) -> Result, Co /// The result is ordered by bucket id, descending. Thus, the first element is the current bucket. pub fn get_claimable_reward_buckets(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - // Adding 1 because we store the future reward bucket in the map also, so grace_period + 1 - let grace_period = Uint64::new(config.grace_period) - .checked_add(Uint64::one())? - .u64(); - // Take grace_period + 1 and then slice last one off + let grace_period = config.grace_period; + let mut reward_buckets = REWARD_BUCKETS .range(deps.storage, None, None, Order::Descending) .take(grace_period as usize) @@ -310,8 +309,6 @@ pub fn get_claimable_reward_buckets(deps: Deps) -> StdResult last_claimed_epoch); } else { - // if the user doesn't have any last_claimed_epoch two things might be happening: - // 1- the user has never bonded before - // 2- the user has bonded, but never claimed any rewards so far - - let bonded_response: BondedResponse = query_bonded(deps, Some(address.into_string()))?; - println!("bonded_responsebonded_response: {:?}", bonded_response); - if bonded_response.bonded_assets.is_empty() { - // the user has never bonded before, therefore it shouldn't be able to claim anything - claimable_reward_buckets.clear(); - } else { - // the user has bonded, but never claimed any rewards so far. The first_bonded_epoch_id - // value should always be Some here, as `query_bonded` will always return Some. - match bonded_response.first_bonded_epoch_id { - Some(first_bonded_epoch_id) => { - // keep all buckets that are newer than the first bonded epoch - claimable_reward_buckets.retain(|bucket| bucket.id > first_bonded_epoch_id); - } - None => { - // for sanity, it should never happen - claimable_reward_buckets.clear(); - } - } - } + // if the user doesn't have any last_claimed_epoch it means it never bonded + claimable_reward_buckets.clear(); }; // filter out buckets that have no available fees. This would only happen in case the grace period // gets increased after buckets have expired, which would lead to make them available for claiming @@ -369,8 +345,6 @@ pub fn query_claimable( claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } - // todo exclude buckets which eopch id is the same as the bond created epoch id - 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 d5227dcf..18aea86b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -122,7 +122,7 @@ pub(crate) fn fill_rewards( // Each of these helpers will add messages to the messages vector // and may increment the distribution_denom Coin above with the result of the swaps - helpers::handle_lp_tokens_rewards(&remnant_coins, &config, &mut submessages)?; + helpers::handle_lp_tokens_rewards(&deps, &remnant_coins, &config, &mut submessages)?; helpers::swap_coins_to_main_token( remnant_coins, &deps, @@ -194,6 +194,7 @@ pub fn handle_lp_withdrawal_reply(deps: DepsMut, msg: Reply) -> Result Result { let claimable_reward_buckets_for_user = query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.reward_buckets; + ensure!( !claimable_reward_buckets_for_user.is_empty(), ContractError::NothingToClaim diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index dd50b684..73321836 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -80,7 +80,7 @@ fn test_bond_successfully() { Some(global_index.clone().into_inner()), BondingWeightResponse { address: sender.to_string(), - weight: Uint128::new(1_000u128), + weight: Uint128::zero(), global_weight: Uint128::zero(), // because the snapshot was taken at the beginning of the epoch share: Decimal::zero(), epoch_id: 1u64, @@ -179,9 +179,9 @@ fn test_bond_successfully() { Some(global_index.clone().into_inner()), BondingWeightResponse { address: sender.to_string(), - weight: Uint128::new(10000u128), - global_weight: Uint128::new(10000u128), - share: Decimal::from_ratio(10000u128, 10000u128), + weight: Uint128::new(6_000u128), + global_weight: Uint128::new(8_000u128), + share: Decimal::from_ratio(6_000u128, 8_000u128), epoch_id: 2u64, }, ) diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 07fd3144..7e205d3c 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -62,7 +62,7 @@ pub struct Bond { pub updated_last: u64, /// The weight of the bond at the given block height. pub weight: Uint128, - //pub previous: (u64, Uint128) + //pub previous: Option<(u64, Uint128)> } impl Default for Bond { @@ -75,6 +75,7 @@ impl Default for Bond { created_at_epoch: Default::default(), updated_last: Default::default(), weight: Uint128::zero(), + //previous: None, } } } From 76aa0f75aa3740006086b7add14e6ac461e39af0 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 15 May 2024 14:07:57 +0100 Subject: [PATCH 30/51] chore: add rewards query --- .../schema/bonding-manager.json | 228 +++++------------- .../bonding-manager/schema/raw/query.json | 128 ++-------- .../schema/raw/response_to_rewards.json | 40 +++ .../bonding-manager/src/bonding/commands.rs | 2 +- .../bonding-manager/src/contract.rs | 55 +++-- .../bonding-manager/src/helpers.rs | 112 ++++++++- .../bonding-manager/src/queries.rs | 49 ++-- .../bonding-manager/src/rewards/commands.rs | 160 +++++------- .../bonding-manager/src/state.rs | 29 +-- .../bonding-manager/src/tests/bond.rs | 6 +- .../bonding-manager/src/tests/instantiate.rs | 22 +- .../bonding-manager/src/tests/mod.rs | 2 +- .../bonding-manager/src/tests/suite.rs | 119 ++------- .../src/tests/update_config.rs | 141 +++++++---- .../white-whale-std/src/bonding_manager.rs | 33 ++- .../white-whale-std/src/pool_network/asset.rs | 2 +- 16 files changed, 479 insertions(+), 649 deletions(-) create mode 100644 contracts/liquidity_hub/bonding-manager/schema/raw/response_to_rewards.json diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 0adaf341..9d4a045a 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -488,41 +488,23 @@ "additionalProperties": false }, { - "description": "Returns the weight of the address.", + "description": "Returns the weight of the address. Returns the global index of the contract.", "type": "object", "required": [ - "weight" + "global_index" ], "properties": { - "weight": { + "global_index": { "type": "object", - "required": [ - "address" - ], "properties": { - "address": { - "description": "The address to check for weight.", - "type": "string" - }, "epoch_id": { - "description": "The timestamp to check for weight. If none is provided, the current block time is used.", + "description": "The epoch id to check for the global index. If none is provided, the current global index is returned.", "type": [ "integer", "null" ], "format": "uint64", "minimum": 0.0 - }, - "global_index": { - "description": "The global index to check for weight. If none is provided, the current global index is used.", - "anyOf": [ - { - "$ref": "#/definitions/GlobalIndex" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -531,23 +513,21 @@ "additionalProperties": false }, { - "description": "Returns the global index of the contract.", + "description": "Returns the [RewardBucket]s that can be claimed by an address.", "type": "object", "required": [ - "global_index" + "claimable" ], "properties": { - "global_index": { + "claimable": { "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.", + "address": { + "description": "The address to check for claimable reward buckets. If none is provided, all possible reward buckets stored in the contract that can potentially be claimed are returned.", "type": [ - "integer", + "string", "null" - ], - "format": "uint64", - "minimum": 0.0 + ] } }, "additionalProperties": false @@ -556,21 +536,20 @@ "additionalProperties": false }, { - "description": "Returns the [RewardBucket]s that can be claimed by an address.", + "description": "Returns the rewards for the given address.", "type": "object", "required": [ - "claimable" + "rewards" ], "properties": { - "claimable": { + "rewards": { "type": "object", + "required": [ + "address" + ], "properties": { "address": { - "description": "The address to check for claimable reward buckets. If none is provided, all possible reward buckets stored in the contract that can potentially be claimed are returned.", - "type": [ - "string", - "null" - ] + "type": "string" } }, "additionalProperties": false @@ -592,76 +571,7 @@ }, "additionalProperties": false } - ], - "definitions": { - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "GlobalIndex": { - "type": "object", - "required": [ - "bonded_amount", - "bonded_assets", - "epoch_id", - "updated_last", - "weight" - ], - "properties": { - "bonded_amount": { - "description": "The total amount of tokens bonded in the contract.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "bonded_assets": { - "description": "Assets that are bonded in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "epoch_id": { - "description": "The epoch id the global index was taken a snapshot for", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "updated_last": { - "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.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } + ] }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -1111,6 +1021,46 @@ } } }, + "rewards": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RewardsResponse", + "description": "Response for the Rewards query", + "type": "object", + "required": [ + "rewards" + ], + "properties": { + "rewards": { + "description": "The rewards that can be claimed by the address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "unbonding": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "UnbondingResponse", @@ -1200,66 +1150,6 @@ } } }, - "weight": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BondingWeightResponse", - "description": "Response for the Weight query.", - "type": "object", - "required": [ - "address", - "epoch_id", - "global_weight", - "share", - "weight" - ], - "properties": { - "address": { - "description": "The weight of the address.", - "type": "string" - }, - "epoch_id": { - "description": "The epoch id at which the weight was calculated.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "global_weight": { - "description": "The global weight of the contract.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "share": { - "description": "The share the address has of the rewards at the particular timestamp.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "weight": { - "description": "The weight of the address at the given timestamp.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, "withdrawable": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "WithdrawableResponse", diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index 25ad45d2..376bbcb4 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -107,50 +107,7 @@ "additionalProperties": false }, { - "description": "Returns the weight of the address.", - "type": "object", - "required": [ - "weight" - ], - "properties": { - "weight": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The address to check for weight.", - "type": "string" - }, - "epoch_id": { - "description": "The timestamp to check for weight. If none is provided, the current block time is used.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "global_index": { - "description": "The global index to check for weight. If none is provided, the current global index is used.", - "anyOf": [ - { - "$ref": "#/definitions/GlobalIndex" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the global index of the contract.", + "description": "Returns the weight of the address. Returns the global index of the contract.", "type": "object", "required": [ "global_index" @@ -198,87 +155,40 @@ "additionalProperties": false }, { - "description": "Query the contract's ownership information", + "description": "Returns the rewards for the given address.", "type": "object", "required": [ - "ownership" + "rewards" ], "properties": { - "ownership": { + "rewards": { "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, "additionalProperties": false } }, "additionalProperties": false - } - ], - "definitions": { - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } }, - "GlobalIndex": { + { + "description": "Query the contract's ownership information", "type": "object", "required": [ - "bonded_amount", - "bonded_assets", - "epoch_id", - "updated_last", - "weight" + "ownership" ], "properties": { - "bonded_amount": { - "description": "The total amount of tokens bonded in the contract.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "bonded_assets": { - "description": "Assets that are bonded in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "epoch_id": { - "description": "The epoch id the global index was taken a snapshot for", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "updated_last": { - "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.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] + "ownership": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" } - } + ] } diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_rewards.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_rewards.json new file mode 100644 index 00000000..cebb6804 --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_rewards.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RewardsResponse", + "description": "Response for the Rewards query", + "type": "object", + "required": [ + "rewards" + ], + "properties": { + "rewards": { + "description": "The rewards that can be claimed by the address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "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 8fc313b8..bcd9cc4f 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -61,7 +61,7 @@ pub(crate) fn bond( global_index.weight = global_index.weight.checked_add(asset.amount)?; global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; global_index.bonded_assets = - asset::aggregate_coins(global_index.bonded_assets, vec![asset.clone()])?; + asset::aggregate_coins(&global_index.bonded_assets, &vec![asset.clone()])?; update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; Ok(Response::default().add_attributes(vec![ diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 6077fce5..bd17d9fb 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -133,38 +133,41 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary( &queries::query_withdrawable(deps, address, denom)?, )?), - QueryMsg::Weight { - address, - epoch_id, - global_index, - } => { - let epoch_id = if let Some(epoch_id) = epoch_id { - epoch_id - } else { - // If epoch_id is not provided, use current epoch - 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; - current_epoch.epoch.id - }; - - Ok(to_json_binary(&queries::query_weight( - deps, - epoch_id, - address, - global_index, - )?)?) - } + // QueryMsg::Weight { + // address, + // epoch_id, + // global_index, + // } => { + // let epoch_id = if let Some(epoch_id) = epoch_id { + // epoch_id + // } else { + // // If epoch_id is not provided, use current epoch + // 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, + // &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + // )?; + // current_epoch.epoch.id + // }; + // + // Ok(to_json_binary(&queries::query_weight( + // deps, + // epoch_id, + // address, + // global_index, + // )?)?) + // } QueryMsg::GlobalIndex { epoch_id } => Ok(to_json_binary(&queries::query_global_index( deps, epoch_id, )?)?), QueryMsg::Claimable { address } => { - Ok(to_json_binary(&queries::query_claimable(deps, address)?)?) + Ok(to_json_binary(&queries::query_claimable(&deps, address)?)?) } QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), + QueryMsg::Rewards { address } => { + Ok(to_json_binary(&queries::query_rewards(deps, address)?)?) + } } } diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index b4695e41..2038a90c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use cosmwasm_std::{ - ensure, to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, MessageInfo, Order, ReplyOn, - StdResult, SubMsg, WasmMsg, + ensure, to_json_binary, Addr, Attribute, Coin, CosmosMsg, Decimal, Deps, DepsMut, MessageInfo, + Order, ReplyOn, StdError, StdResult, SubMsg, WasmMsg, }; use cw_utils::PaymentError; @@ -10,10 +12,11 @@ use white_whale_std::epoch_manager::epoch_manager::EpochResponse; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, }; +use white_whale_std::pool_network::asset::aggregate_coins; use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; -use crate::queries::query_claimable; +use crate::queries::{query_claimable, query_weight}; use crate::state::{CONFIG, REWARD_BUCKETS}; /// Validates that the growth rate is between 0 and 1. @@ -57,7 +60,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())).unwrap(); // ensure the user has nothing to claim ensure!( claimable_rewards.reward_buckets.is_empty(), @@ -268,3 +271,104 @@ pub(crate) fn validate_buckets_not_empty(deps: &DepsMut) -> Result<(), ContractE Ok(()) } + +type ClaimableRewards = Vec; +// key is reward id, value is the rewards claimed from that bucket +type ModifiedRewardBuckets = HashMap>; + +/// Calculates the rewards for a user. +pub fn calculate_rewards( + deps: &Deps, + address: Addr, + is_claim: bool, +) -> Result<(ClaimableRewards, Vec, ModifiedRewardBuckets), ContractError> { + let claimable_reward_buckets_for_user = + query_claimable(deps, Some(address.to_string()))?.reward_buckets; + + // if the function is being called from the claim function + if is_claim { + ensure!( + !claimable_reward_buckets_for_user.is_empty(), + ContractError::NothingToClaim + ); + } else { + // if the function is being called from the rewards query + if claimable_reward_buckets_for_user.is_empty() { + return Ok((vec![], vec![], HashMap::new())); + } + } + + let mut total_claimable_rewards = vec![]; + let mut attributes = vec![]; + let mut modified_reward_buckets = HashMap::new(); + + for reward_bucket in claimable_reward_buckets_for_user { + let bonding_weight_response_for_epoch = query_weight( + deps, + reward_bucket.id, + address.to_string(), + Some(reward_bucket.global_index.clone()), + )?; + + // if the user has no share in the bucket, skip it + if bonding_weight_response_for_epoch.share.is_zero() { + continue; + }; + + // sanity check + ensure!( + bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), + ContractError::InvalidShare + ); + + let mut claimed_rewards_from_bucket = vec![]; + + for reward in reward_bucket.total.iter() { + let user_reward = reward.amount * bonding_weight_response_for_epoch.share; + + // make sure the reward is sound + let reward_validation: Result<(), StdError> = reward_bucket + .available + .iter() + .find(|available_fee| available_fee.denom == reward.denom) + .map(|available_reward| { + if user_reward > available_reward.amount { + attributes.push(Attribute { + key: "error".to_string(), + value: ContractError::InvalidReward { + reward: user_reward, + available: available_reward.amount, + } + .to_string(), + }); + return Err(StdError::generic_err("Invalid fee")); + } + Ok(()) + }) + .ok_or(StdError::generic_err("Invalid fee"))?; + + // if the reward is invalid, skip the bucket + match reward_validation { + Ok(_) => {} + Err(_) => continue, + } + + let reward = Coin { + denom: reward.denom.to_string(), + amount: user_reward, + }; + + // add the reward + total_claimable_rewards = + aggregate_coins(&total_claimable_rewards, &vec![reward.clone()])?; + + if is_claim { + claimed_rewards_from_bucket = + aggregate_coins(&claimed_rewards_from_bucket, &vec![reward])?; + modified_reward_buckets + .insert(reward_bucket.id, claimed_rewards_from_bucket.clone()); + } + } + } + Ok((total_claimable_rewards, attributes, modified_reward_buckets)) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 249f5603..a2955cd6 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,12 +1,12 @@ -use std::collections::{HashSet, VecDeque}; +use std::collections::VecDeque; use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Uint128}; use cw_storage_plus::Bound; -use crate::ContractError; +use crate::{helpers, ContractError}; use white_whale_std::bonding_manager::{ - Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, UnbondingResponse, - WithdrawableResponse, + Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, RewardsResponse, + UnbondingResponse, WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; @@ -148,7 +148,7 @@ pub(crate) fn query_withdrawable( /// Queries the current weight of the given address. pub(crate) fn query_weight( - deps: Deps, + deps: &Deps, epoch_id: u64, address: String, global_index: Option, @@ -165,31 +165,10 @@ pub(crate) fn query_weight( println!("bonds: {:?}", bonds); 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, - &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - )?; let mut total_bond_weight = Uint128::zero(); - // Search bonds for unique bond.asset.denoms - // Make an empty set of unique denoms - let mut unique_denoms: HashSet = HashSet::new(); for (_, mut bond) in bonds? { - println!("bond-before: {:?}", bond); - - if bond.updated_last == current_epoch.epoch.id { - continue; - } - // - // println!("bond.previous.is_some(): {:?} ", bond.previous.is_some() && bond.previous.unwrap().0 == current_epoch.epoch.id); - // //todo - // if bond.created_at_epoch == current_epoch.epoch.id || - // bond.previous.is_some() && bond.previous.unwrap().0 == current_epoch.epoch.id { - // continue; - // } - bond.weight = get_weight( epoch_id, bond.weight, @@ -198,12 +177,6 @@ pub(crate) fn query_weight( bond.updated_last, )?; - println!("bond-after: {:?}", bond); - - if !unique_denoms.contains(&bond.asset.denom) { - unique_denoms.insert(bond.asset.denom.clone()); - println!("unique_denoms: {:?}", unique_denoms); - } // 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)?; @@ -293,7 +266,7 @@ pub fn get_expiring_reward_bucket(deps: Deps) -> Result, Co /// Returns the buckets that are within the grace period, i.e. the ones which fees can still be claimed. /// The result is ordered by bucket id, descending. Thus, the first element is the current bucket. -pub fn get_claimable_reward_buckets(deps: Deps) -> StdResult { +pub fn get_claimable_reward_buckets(deps: &Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; let grace_period = config.grace_period; @@ -321,7 +294,7 @@ pub fn get_claimable_reward_buckets(deps: Deps) -> StdResult, ) -> StdResult { let mut claimable_reward_buckets = get_claimable_reward_buckets(deps)?.reward_buckets; @@ -349,3 +322,11 @@ pub fn query_claimable( reward_buckets: claimable_reward_buckets, }) } + +/// Returns the rewards that can be claimed by the given address. +pub(crate) fn query_rewards(deps: Deps, address: String) -> Result { + let (rewards, _, _) = + helpers::calculate_rewards(&deps, deps.api.addr_validate(&address)?, false)?; + + Ok(RewardsResponse { rewards }) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs index 18aea86b..029f2916 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - ensure, from_json, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Reply, - Response, StdError, SubMsg, Uint128, + ensure, from_json, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Reply, Response, + SubMsg, Uint128, }; use cw_utils::parse_reply_execute_data; @@ -8,7 +8,7 @@ use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket, UpcomingReward use white_whale_std::epoch_manager::epoch_manager::Epoch; use white_whale_std::pool_network::asset; -use crate::queries::{get_expiring_reward_bucket, query_claimable, query_weight}; +use crate::queries::get_expiring_reward_bucket; use crate::state::{ fill_upcoming_reward_bucket, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET, @@ -66,11 +66,11 @@ pub(crate) fn on_epoch_created( if let Some(expiring_bucket) = expiring_reward_bucket.as_mut() { // Aggregate the available assets from the expiring bucket to the new reward bucket new_reward_bucket.available = asset::aggregate_coins( - new_reward_bucket.available, - expiring_bucket.available.clone(), + &new_reward_bucket.available, + &expiring_bucket.available.clone(), )?; new_reward_bucket.total = - asset::aggregate_coins(new_reward_bucket.total, expiring_bucket.available.clone())?; + asset::aggregate_coins(&new_reward_bucket.total, &expiring_bucket.available.clone())?; // Set the available assets for the expiring bucket to an empty vec now that they have been // forwarded @@ -192,121 +192,71 @@ pub fn handle_lp_withdrawal_reply(deps: DepsMut, msg: Reply) -> Result Result { - let claimable_reward_buckets_for_user = - query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.reward_buckets; - - ensure!( - !claimable_reward_buckets_for_user.is_empty(), - ContractError::NothingToClaim - ); - - let mut claimable_rewards = vec![]; - let mut attributes = vec![]; - for mut reward_bucket in claimable_reward_buckets_for_user.clone() { - let bonding_weight_response_for_epoch = query_weight( - deps.as_ref(), - reward_bucket.id, - info.sender.to_string(), - Some(reward_bucket.global_index.clone()), - )?; - - // if the user has no share in the bucket, skip it - if bonding_weight_response_for_epoch.share.is_zero() { - continue; - }; + let (total_claimable_rewards, attributes, modified_reward_buckets) = + helpers::calculate_rewards(&deps.as_ref(), info.sender.clone(), true)?; + + // Save the modified reward buckets + for (reward_id, claimed_rewards) in modified_reward_buckets { + REWARD_BUCKETS.update( + deps.storage, + reward_id, + |reward_bucket| -> Result<_, ContractError> { + let mut reward_bucket = reward_bucket.unwrap(); + + for reward in claimed_rewards.iter() { + for available_fee in reward_bucket.available.iter_mut() { + if available_fee.denom == reward.denom { + available_fee.amount = + available_fee.amount.saturating_sub(reward.amount); + } + } - // sanity check - ensure!( - bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), - ContractError::InvalidShare - ); - - for reward in reward_bucket.total.iter() { - let user_reward = reward.amount * bonding_weight_response_for_epoch.share; - - // make sure the reward is sound - let reward_validation: Result<(), StdError> = reward_bucket - .available - .iter() - .find(|available_fee| available_fee.denom == reward.denom) - .map(|available_fee| { - if user_reward > available_fee.amount { - attributes.push(( - "error", - ContractError::InvalidReward { - reward: user_reward, - available: available_fee.amount, + if reward_bucket.claimed.is_empty() { + reward_bucket.claimed = vec![Coin { + denom: reward.denom.clone(), + amount: reward.amount, + }]; + } else { + for claimed_reward in reward_bucket.claimed.iter_mut() { + if claimed_reward.denom == reward.denom { + claimed_reward.amount = + claimed_reward.amount.checked_add(reward.amount)?; } - .to_string(), - )); - } - Ok(()) - }) - .ok_or(StdError::generic_err("Invalid fee"))?; - - // if the reward is invalid, skip the bucket - match reward_validation { - Ok(_) => {} - Err(_) => continue, - } - - let denom = &reward.denom; - // add the reward - claimable_rewards = asset::aggregate_coins( - claimable_rewards, - vec![Coin { - denom: denom.to_string(), - amount: user_reward, - }], - )?; - - // modify the bucket to reflect the new available and claimed amount - for available_fee in reward_bucket.available.iter_mut() { - if available_fee.denom == reward.denom { - available_fee.amount = available_fee.amount.saturating_sub(user_reward); - } - } - - if reward_bucket.claimed.is_empty() { - reward_bucket.claimed = vec![Coin { - denom: denom.to_string(), - amount: user_reward, - }]; - } else { - for claimed_reward in reward_bucket.claimed.iter_mut() { - if claimed_reward.denom == reward.denom { - claimed_reward.amount = claimed_reward.amount.checked_add(user_reward)?; - } - // sanity check, should never happen - for total_reward in reward_bucket.total.iter() { - if total_reward.denom == claimed_reward.denom { - ensure!( - claimed_reward.amount <= total_reward.amount, - ContractError::InvalidShare - ); + // sanity check, should never happen + for total_reward in reward_bucket.total.iter() { + if total_reward.denom == claimed_reward.denom { + ensure!( + claimed_reward.amount <= total_reward.amount, + ContractError::InvalidShare + ); + } + } } } } - } - REWARD_BUCKETS.save(deps.storage, reward_bucket.id, &reward_bucket)?; - } + Ok(reward_bucket) + }, + )?; } // update the last claimed epoch for the user. it's in the first bucket on the list since it's sorted // in descending order - LAST_CLAIMED_EPOCH.save( - deps.storage, - &info.sender, - &claimable_reward_buckets_for_user[0].id, - )?; + 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, + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; + + LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.epoch.id)?; Ok(Response::default() .add_attributes(vec![("action", "claim".to_string())]) .add_attributes(attributes) .add_message(CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), - amount: claimable_rewards, + amount: total_claimable_rewards, }))) } diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 5fa419d8..8598f976 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,5 +1,5 @@ use crate::ContractError; -use cosmwasm_std::{Addr, Coin, Decimal, Deps, DepsMut, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Addr, Coin, Decimal, DepsMut, StdError, StdResult, Uint128}; use cw_storage_plus::{Item, Map}; use white_whale_std::bonding_manager::{ Bond, Config, GlobalIndex, RewardBucket, UpcomingRewardBucket, @@ -91,35 +91,10 @@ pub fn get_weight( Ok(weight.checked_add(amount.checked_mul(time_factor)? * growth_rate)?) } -/// Returns the epoch that is falling out the grace period, which is the one expiring after creating -/// a new epoch is created. -pub fn get_expiring_epoch(deps: Deps) -> StdResult> { - let grace_period = CONFIG.load(deps.storage)?.grace_period; - - // last epochs within the grace period - let epochs = REWARD_BUCKETS - .range(deps.storage, None, None, Order::Descending) - .take(grace_period as usize) - .map(|item| { - let (_, epoch) = item?; - Ok(epoch) - }) - .collect::>>()?; - - // if the epochs vector's length is the same as the grace period it means there is one epoch that - // is expiring once the new one is created i.e. the last epoch in the vector - if epochs.len() == grace_period as usize { - Ok(Some(epochs.last().cloned().unwrap_or_default())) - } else { - // nothing is expiring yet - Ok(None) - } -} - /// Fills the upcoming reward bucket with the given funds. pub fn fill_upcoming_reward_bucket(deps: DepsMut, funds: Coin) -> StdResult<()> { UPCOMING_REWARD_BUCKET.update(deps.storage, |mut upcoming_bucket| -> StdResult<_> { - upcoming_bucket.total = asset::aggregate_coins(upcoming_bucket.total, vec![funds])?; + upcoming_bucket.total = asset::aggregate_coins(&upcoming_bucket.total, &vec![funds])?; Ok(upcoming_bucket) })?; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index 73321836..33fae6db 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -125,7 +125,7 @@ fn test_bond_successfully() { .query_global_index(Some(2u64), |res| { let gi = res.unwrap().1; println!("gi 2:: {:?}", gi); - *global_index.borrow_mut() = gi.clone(); + *global_index.borrow_mut() = gi.clone(); // 8000 weight }); suite @@ -150,7 +150,7 @@ fn test_bond_successfully() { }, ) .assert_bonded_response( - sender.to_string(), + sender.to_string(), // weight -> 10k BondedResponse { total_bonded: Uint128::new(5_000u128), bonded_assets: vec![ @@ -176,7 +176,7 @@ fn test_bond_successfully() { .assert_bonding_weight_response( sender.to_string(), Some(2u64), - Some(global_index.clone().into_inner()), + None, BondingWeightResponse { address: sender.to_string(), weight: Uint128::new(6_000u128), diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index 7516ffa1..c52b0379 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -17,14 +17,20 @@ fn test_instantiate_successfully() { vec!["ampWHALE".to_string(), "bWHALE".to_string()], &vec![], ) - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 1u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 1u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index ca4721da..92ba66f5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -1,4 +1,4 @@ -mod bond; +// mod bond; // mod claim; mod instantiate; // mod rewards; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 5e9a6e49..1849c53e 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -15,7 +15,7 @@ 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, UnbondingResponse, WithdrawableResponse, + QueryMsg, RewardsResponse, UnbondingResponse, WithdrawableResponse, }; use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use white_whale_std::epoch_manager::epoch_manager::{Epoch as EpochV2, EpochConfig}; @@ -441,32 +441,6 @@ impl TestingSuite { self } - #[track_caller] - pub(crate) fn query_weight( - &mut self, - address: String, - epoch_id: Option, - global_index: Option, - response: impl Fn(StdResult<(&mut Self, BondingWeightResponse)>), - ) -> &mut Self { - let bonding_weight_response: BondingWeightResponse = self - .app - .wrap() - .query_wasm_smart( - &self.bonding_manager_addr, - &QueryMsg::Weight { - address, - epoch_id, - global_index, - }, - ) - .unwrap(); - - response(Ok((self, bonding_weight_response))); - - self - } - #[track_caller] pub(crate) fn query_global_index( &mut self, @@ -576,6 +550,23 @@ impl TestingSuite { self } + #[track_caller] + pub(crate) fn query_rewards( + &mut self, + address: String, + response: impl Fn(StdResult<(&mut Self, RewardsResponse)>), + ) -> &mut Self { + let rewards_response: RewardsResponse = self + .app + .wrap() + .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::Rewards { address }) + .unwrap(); + + response(Ok((self, rewards_response))); + + self + } + // Pool Manager methods #[track_caller] @@ -695,77 +686,3 @@ impl TestingSuite { self } } - -/// assertions -impl TestingSuite { - #[track_caller] - pub(crate) fn assert_config(&mut self, expected: Config) -> &mut Self { - self.query_config(|res| { - let config = res.unwrap().1; - assert_eq!(config, expected); - }); - - self - } - - #[track_caller] - pub(crate) fn assert_owner(&mut self, expected: String) -> &mut Self { - self.query_owner(|res| { - let owner = res.unwrap().1; - assert_eq!(owner, expected); - }); - - self - } - #[track_caller] - pub(crate) fn assert_bonded_response( - &mut self, - address: String, - expected: BondedResponse, - ) -> &mut Self { - self.query_bonded(Some(address), |res| { - let bonded_response = res.unwrap().1; - assert_eq!(bonded_response, expected); - }) - } - - #[track_caller] - pub(crate) fn assert_bonding_weight_response( - &mut self, - address: String, - epoch_id: Option, - global_index: Option, - expected: BondingWeightResponse, - ) -> &mut Self { - self.query_weight(address, epoch_id, global_index, |res| { - let bonding_weight_response = res.unwrap().1; - assert_eq!(bonding_weight_response, expected); - }) - } - - #[track_caller] - pub(crate) fn assert_unbonding_response( - &mut self, - address: String, - denom: String, - expected: UnbondingResponse, - ) -> &mut Self { - self.query_unbonding(address, denom, None, None, |res| { - let unbonding_response = res.unwrap().1; - assert_eq!(unbonding_response, expected); - }) - } - - #[track_caller] - pub(crate) fn assert_withdrawable_response( - &mut self, - address: String, - denom: String, - expected: WithdrawableResponse, - ) -> &mut Self { - self.query_withdrawable(address, denom, |res| { - let withdrawable_response = res.unwrap().1; - assert_eq!(withdrawable_response, expected); - }) - } -} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index 0ff9c00b..7f6dd3e2 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -1,9 +1,9 @@ -use cosmwasm_std::{Addr, Decimal, Uint128, Uint64}; +use cosmwasm_std::{Addr, Decimal, Uint128}; -use crate::ContractError; use white_whale_std::bonding_manager::Config; use crate::tests::suite::TestingSuite; +use crate::ContractError; #[test] fn test_update_config_successfully() { @@ -12,16 +12,25 @@ fn test_update_config_successfully() { suite .instantiate_default() - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 1u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 1u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); + }) + .query_owner(|res| { + let owner = res.unwrap().1; + assert_eq!(owner, "migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); }) - .assert_owner("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3".to_string()) .update_config( owner.clone(), None, @@ -33,14 +42,20 @@ fn test_update_config_successfully() { )), |_res| {}, ) - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 500u64, - growth_rate: Decimal::from_ratio(Uint128::new(1u128), Uint128::new(2u128)), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 500u64, + growth_rate: Decimal::from_ratio(Uint128::new(1u128), Uint128::new(2u128)), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }) .update_config( owner, @@ -50,14 +65,20 @@ fn test_update_config_successfully() { Some(Decimal::one()), |_res| {}, ) - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract6"), - epoch_manager_addr: Addr::unchecked("contract5"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 500u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract6"), + epoch_manager_addr: Addr::unchecked("contract5"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 500u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }); } @@ -68,14 +89,20 @@ fn test_update_config_unsuccessfully() { suite .instantiate_default() - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 1u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 1u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }) .update_config( Addr::unchecked("unauthorized"), @@ -96,14 +123,20 @@ fn test_update_config_unsuccessfully() { } }, ) - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 1u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 1u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }) .update_config( owner, @@ -124,13 +157,19 @@ fn test_update_config_unsuccessfully() { } }, ) - .assert_config(Config { - pool_manager_addr: Addr::unchecked("contract2"), - epoch_manager_addr: Addr::unchecked("contract0"), - distribution_denom: "uwhale".to_string(), - unbonding_period: 1u64, - growth_rate: Decimal::one(), - grace_period: 21u64, - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + .query_config(|res| { + let config = res.unwrap().1; + assert_eq!( + config, + Config { + pool_manager_addr: Addr::unchecked("contract2"), + epoch_manager_addr: Addr::unchecked("contract0"), + distribution_denom: "uwhale".to_string(), + unbonding_period: 1u64, + growth_rate: Decimal::one(), + grace_period: 21u64, + bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], + } + ); }); } diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 7e205d3c..646022e4 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -199,16 +199,18 @@ pub enum QueryMsg { 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(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)] @@ -218,6 +220,7 @@ pub enum QueryMsg { epoch_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 { @@ -225,11 +228,23 @@ 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 }, } #[cw_serde] pub struct MigrateMsg {} +/// Response for the Rewards query +#[cw_serde] +pub struct RewardsResponse { + /// The rewards that can be claimed by the address. + pub rewards: Vec, +} + /// Response for the Bonded query #[cw_serde] pub struct BondedResponse { diff --git a/packages/white-whale-std/src/pool_network/asset.rs b/packages/white-whale-std/src/pool_network/asset.rs index e2ade499..a30560c3 100644 --- a/packages/white-whale-std/src/pool_network/asset.rs +++ b/packages/white-whale-std/src/pool_network/asset.rs @@ -568,7 +568,7 @@ pub fn aggregate_assets(assets: Vec, other_assets: Vec) -> StdResu } /// Aggregates assets from two vectors, summing up the amounts of assets that are the same. -pub fn aggregate_coins(coins: Vec, other_coins: Vec) -> StdResult> { +pub fn aggregate_coins(coins: &Vec, other_coins: &Vec) -> StdResult> { let mut aggregated_coins: Vec = Vec::with_capacity(coins.len() + other_coins.len()); for coin in coins { aggregated_coins.push(coin.clone()); From ff9c23f588005249c12806322600d0259fd3e4c0 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 15 May 2024 14:42:32 +0100 Subject: [PATCH 31/51] test: enable rewards test --- .../bonding-manager/src/bonding/commands.rs | 27 +- .../bonding-manager/src/error.rs | 7 +- .../bonding-manager/src/helpers.rs | 11 +- .../bonding-manager/src/queries.rs | 28 +- .../bonding-manager/src/rewards/commands.rs | 24 +- .../bonding-manager/src/state.rs | 12 +- .../bonding-manager/src/tests/claim.rs | 1180 ++++++++++++----- .../bonding-manager/src/tests/mod.rs | 4 +- .../bonding-manager/src/tests/rewards.rs | 132 +- .../bonding-manager/src/tests/suite.rs | 25 +- .../src/tests/update_config.rs | 4 +- .../white-whale-std/src/bonding_manager.rs | 10 +- 12 files changed, 1042 insertions(+), 422 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index bcd9cc4f..4c91fc10 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -39,30 +39,33 @@ pub(crate) fn bond( ..asset.clone() }, created_at_epoch: current_epoch.epoch.id, - updated_last: current_epoch.epoch.id, + last_updated: current_epoch.epoch.id, ..Bond::default() }); - // if bond.updated_last != current_epoch.epoch.id.clone() { - // bond.previous = Some((bond.updated_last, bond.weight)); - // } - // update local 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)?; - update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; - LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.epoch.id)?; + BOND.save(deps.storage, (&info.sender, &bond.asset.denom), &bond)?; // update global values - let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); + let mut global_index = GLOBAL.load(deps.storage)?; // include time term in the weight - global_index.weight = global_index.weight.checked_add(asset.amount)?; + 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 = asset::aggregate_coins(&global_index.bonded_assets, &vec![asset.clone()])?; - update_global_weight(&mut deps, current_epoch.epoch.id, global_index)?; + + GLOBAL.save(deps.storage, &global_index)?; + + LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.epoch.id)?; Ok(Response::default().add_attributes(vec![ ("action", "bond".to_string()), @@ -126,7 +129,7 @@ pub(crate) fn unbond( &Bond { asset: asset.clone(), weight: Uint128::zero(), - updated_last: current_epoch.epoch.id, + last_updated: current_epoch.epoch.id, created_at_epoch: current_epoch.epoch.id, //previous: None, }, @@ -137,7 +140,7 @@ pub(crate) fn unbond( global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); global_index.bonded_assets = white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index.weight = global_index.weight.saturating_sub(weight_slash); + global_index.last_weight = global_index.last_weight.saturating_sub(weight_slash); GLOBAL.save(deps.storage, &global_index)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/error.rs b/contracts/liquidity_hub/bonding-manager/src/error.rs index 43f8e65c..72e0bf27 100644 --- a/contracts/liquidity_hub/bonding-manager/src/error.rs +++ b/contracts/liquidity_hub/bonding-manager/src/error.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{DivideByZeroError, OverflowError, StdError, Uint128}; +use cosmwasm_std::{ + CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, Uint128, +}; use cw_ownable::OwnershipError; use cw_utils::PaymentError; use semver::Version; @@ -18,6 +20,9 @@ pub enum ContractError { #[error("{0}")] PaymentError(#[from] PaymentError), + #[error("{0}")] + CheckedMultiplyFractionError(#[from] CheckedMultiplyFractionError), + #[error("Semver parsing error: {0}")] SemVer(String), diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 2038a90c..46fb04ec 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -178,8 +178,6 @@ pub fn swap_coins_to_main_token( }) .collect(); for coin in coins_to_swap { - println!("Swapping {} to {}", coin.denom, distribution_denom); - // Query for the routes and pool let swap_routes_response: StdResult = deps.querier.query_wasm_smart( config.pool_manager_addr.to_string(), @@ -189,7 +187,6 @@ pub fn swap_coins_to_main_token( }, ); - println!("swap_routes_response: {:?}", swap_routes_response); let swap_routes = match swap_routes_response { Ok(swap_routes) => swap_routes, // no routes, skip @@ -233,6 +230,7 @@ pub fn swap_coins_to_main_token( operations: swap_routes.swap_route.swap_operations.clone(), }, )?; + // Add the simulate amount received to the distribution_denom amount, if the swap fails this should // also be rolled back to_be_distribution_asset.amount = to_be_distribution_asset @@ -310,7 +308,7 @@ pub fn calculate_rewards( Some(reward_bucket.global_index.clone()), )?; - // if the user has no share in the bucket, skip it + // sanity check, if the user has no share in the bucket, skip it if bonding_weight_response_for_epoch.share.is_zero() { continue; }; @@ -324,7 +322,9 @@ pub fn calculate_rewards( let mut claimed_rewards_from_bucket = vec![]; for reward in reward_bucket.total.iter() { - let user_reward = reward.amount * bonding_weight_response_for_epoch.share; + let user_reward = reward + .amount + .checked_mul_floor(bonding_weight_response_for_epoch.share)?; // make sure the reward is sound let reward_validation: Result<(), StdError> = reward_bucket @@ -332,6 +332,7 @@ pub fn calculate_rewards( .iter() .find(|available_fee| available_fee.denom == reward.denom) .map(|available_reward| { + // sanity check if user_reward > available_reward.amount { attributes.push(Attribute { key: "error".to_string(), diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index a2955cd6..423658f0 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -168,13 +168,15 @@ pub(crate) fn query_weight( let mut total_bond_weight = Uint128::zero(); + println!("epoch id: {:?}", epoch_id); + for (_, mut bond) in bonds? { bond.weight = get_weight( epoch_id, bond.weight, bond.asset.amount, config.growth_rate, - bond.updated_last, + bond.last_updated, )?; // Aggregate the weights of all the bonds for the given address. @@ -196,28 +198,30 @@ pub(crate) fn query_weight( println!("global_index: {:?}", global_index); - global_index.weight = get_weight( + global_index.last_weight = get_weight( epoch_id, - global_index.weight, + global_index.last_weight, global_index.bonded_amount, config.growth_rate, - global_index.updated_last, + 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.weight.is_zero() { + let share = if global_index.last_weight.is_zero() { Decimal::zero() } else { - Decimal::from_ratio(total_bond_weight, global_index.weight) + Decimal::from_ratio(total_bond_weight, global_index.last_weight) }; + println!("share: {:?}", share); + Ok(BondingWeightResponse { address: address.to_string(), weight: total_bond_weight, - global_weight: global_index.weight, + global_weight: global_index.last_weight, share, epoch_id, }) @@ -280,12 +284,8 @@ pub fn get_claimable_reward_buckets(deps: &Deps) -> StdResult>>()?; - println!("reward_buckets: {:?}", reward_buckets); - reward_buckets.retain(|bucket| !bucket.available.is_empty()); - println!("reward_buckets: {:?}", reward_buckets); - Ok(ClaimableRewardBucketsResponse { reward_buckets: reward_buckets.into(), }) @@ -298,6 +298,8 @@ 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 { @@ -305,6 +307,8 @@ pub fn query_claimable( 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); @@ -318,6 +322,8 @@ 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 029f2916..992fec00 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -25,7 +25,10 @@ pub(crate) fn on_epoch_created( ) -> Result { cw_utils::nonpayable(&info)?; - println!("----on_epoch_created----"); + 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)?; @@ -39,7 +42,7 @@ pub(crate) fn on_epoch_created( if global.is_none() { let initial_global_index = GlobalIndex { epoch_id: current_epoch.id, - updated_last: current_epoch.id, + last_updated: current_epoch.id, ..Default::default() }; GLOBAL.save(deps.storage, &initial_global_index)?; @@ -49,6 +52,14 @@ pub(crate) fn on_epoch_created( let mut global_index = GLOBAL.load(deps.storage)?; global_index.epoch_id = current_epoch.id; + if global_index.bonded_amount == Uint128::zero() { + global_index.last_updated = current_epoch.id; + } + + 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)?; @@ -161,6 +172,7 @@ pub fn handle_lp_withdrawal_reply(deps: DepsMut, msg: Reply) -> Result Result Result Result Uint128::zero()); + if reward_bucket.claimed.is_empty() { reward_bucket.claimed = vec![Coin { denom: reward.denom.clone(), diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 8598f976..fb3523d6 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -35,10 +35,10 @@ pub fn update_bond_weight( bond.weight, bond.asset.amount, config.growth_rate, - bond.updated_last, + bond.last_updated, )?; - bond.updated_last = current_epoch_id; + bond.last_updated = current_epoch_id; BOND.save(deps.storage, (&address, &bond.asset.denom), &bond)?; println!("updated bond: {:?}", bond); @@ -54,15 +54,15 @@ pub fn update_global_weight( ) -> Result { let config = CONFIG.load(deps.storage)?; - global_index.weight = get_weight( + global_index.last_weight = get_weight( current_epoch_id, - global_index.weight, + global_index.last_weight, global_index.bonded_amount, config.growth_rate, - global_index.updated_last, + global_index.last_updated, )?; - global_index.updated_last = current_epoch_id; + global_index.last_updated = current_epoch_id; GLOBAL.save(deps.storage, &global_index)?; println!("updated global_index: {:?}", global_index); diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs index 373f775d..91654890 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -1,23 +1,20 @@ -use std::collections::VecDeque; +use cosmwasm_std::coin; +use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; +use std::cell::RefCell; -use cosmwasm_std::{coin, Uint64}; +use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket}; use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; use crate::tests::suite::TestingSuite; -use crate::tests::test_helpers; -use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; - use crate::ContractError; -use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse, Epoch, GlobalIndex}; - -use super::test_helpers::get_epochs; #[test] -fn test_claimable_epochs() { - let mut robot = TestingSuite::default(); - let grace_period = Uint64::new(21); - let creator = robot.sender.clone(); +fn test_claim_successfully() { + 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()]; @@ -35,12 +32,11 @@ fn test_claimable_epochs() { extra_fees: vec![], }; - robot + suite .instantiate_default() .fast_forward(259_200) - .create_epoch(|result| { - result.unwrap(); - }) + // epoch 1 + .create_new_epoch() .create_pair( creator.clone(), asset_denoms.clone(), @@ -109,9 +105,8 @@ fn test_claimable_epochs() { result.unwrap(); }, ) - .create_epoch(|result| { - result.unwrap(); - }) + // epoch 2 + .create_new_epoch() .swap( creator.clone(), coin(2_000u128, "uusdc"), @@ -145,182 +140,112 @@ fn test_claimable_epochs() { }, ); - let expected_epochs = vec![Epoch { - id: Uint64::new(2u64), - start_time: Timestamp::from_nanos(1571970229879305533), - total: vec![coin(1009u128, "uwhale")], - available: vec![coin(1009u128, "uwhale")], - claimed: vec![], - global_index: GlobalIndex { - bonded_amount: Default::default(), - bonded_assets: vec![], - timestamp: Timestamp::from_nanos(1572056619879305533), - weight: Default::default(), - }, - }]; - - robot.query_claimable_epochs(None, |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), expected_epochs.len()); - - for (index, epoch) in epochs.iter().enumerate() { - assert_eq!(expected_epochs[index], epoch.clone()); - } - }); -} + suite + .query_claimable_reward_buckets(None, |res| { + let (_, claimable_reward_buckets) = res.unwrap(); + assert_eq!(claimable_reward_buckets.len(), 1usize); -#[test] -fn test_claim_successfully() { - let mut robot = TestingSuite::default(); - let sender = robot.sender.clone(); - let another_sender = robot.another_sender.clone(); - let asset_infos = vec!["uwhale".to_string(), "uusdc".to_string()]; + assert_eq!( + claimable_reward_buckets[0], + 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(), + }, + } + ); + }) + // epoch 3 + .create_new_epoch() + .query_claimable_reward_buckets(None, |res| { + let (_, claimable_reward_buckets) = res.unwrap(); + assert_eq!(claimable_reward_buckets.len(), 2usize); - // Default Pool fees white_whale_std::pool_network::pair::PoolFee - // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% - #[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![], - }; + assert_eq!( + claimable_reward_buckets, + vec![ + 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(), + }, + }, + ] + ); + }); - robot - .instantiate_default() + // we bond tokens with the creator + suite .bond( - sender.clone(), + creator.clone(), Coin { denom: "ampWHALE".to_string(), amount: Uint128::new(1_000u128), }, &coins(1_000u128, "ampWHALE"), - |_res| {}, - ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(1_000u128), - bonded_assets: vec![Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }], - first_bonded_epoch_id: Default::default(), - }, - ) - .fast_forward(100_000u64) - .create_epoch(|result| { - result.unwrap(); - }) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(100_001_000u128), - global_weight: Uint128::new(100_001_000u128), - share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571897419879305533u64), - }, - ) - .bond( - sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), - }, - &coins(3_000u128, "bWHALE"), - |_res| {}, - ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(4_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), - }, - ], - first_bonded_epoch_id: Default::default(), + |res| { + res.unwrap(); }, ) - .query_total_bonded(|res| { - let bonded_response = res.unwrap().1; - assert_eq!( - bonded_response, - BondedResponse { - total_bonded: Uint128::new(4_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), - }, - ], - first_bonded_epoch_id: Default::default(), - } - ) + .query_rewards(creator.clone().to_string(), |res| { + let (_, rewards) = res.unwrap(); + // empty as the user just bonded + assert!(rewards.rewards.is_empty()); }); - robot.query_claimable_epochs_live(Some(sender.clone()), |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 0); - }); - - println!("-------"); - robot - .create_pair( - sender.clone(), - asset_infos.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( - sender.clone(), - "whale-uusdc".to_string(), - vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1000000000u128), - }, - Coin { - denom: "uusdc".to_string(), - amount: Uint128::from(1000000000u128), - }, - ], - |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(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) - .to_string() - }) - })); - }, - ) + // make some swaps to collect fees for the next epoch + suite .swap( - sender.clone(), - coin(1_000u128, "uusdc"), + creator.clone(), + coin(20_000u128, "uusdc"), "uwhale".to_string(), None, None, @@ -328,15 +253,15 @@ fn test_claim_successfully() { "whale-uusdc".to_string(), vec![Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1_000u128), + amount: Uint128::from(20_000u128), }], |result| { result.unwrap(); }, ) .swap( - sender.clone(), - coin(1_000u128, "uwhale"), + creator.clone(), + coin(20_000u128, "uwhale"), "uusdc".to_string(), None, None, @@ -344,19 +269,116 @@ fn test_claim_successfully() { "whale-uusdc".to_string(), vec![Coin { denom: "uwhale".to_string(), - amount: Uint128::from(1_000u128), + amount: Uint128::from(20_000u128), }], |result| { result.unwrap(); }, + ); + + suite + .add_one_day() + // epoch 4 + .create_new_epoch() + .query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; + + assert_eq!( + claimable_reward_buckets, + vec![ + 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(), + }, + }, + ] + ); + }); + + // we bond more tokens with another user + + suite + .fast_forward(20_000) + .bond( + another_sender.clone(), + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(700u128), + }, + &coins(700u128, "bWHALE"), + |res| { + res.unwrap(); + }, ) - .fast_forward(90_000) - .create_epoch(|result| { - result.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( - sender.clone(), - coin(1_000u128, "uusdc"), + creator.clone(), + coin(100_000u128, "uusdc"), "uwhale".to_string(), None, None, @@ -364,91 +386,162 @@ fn test_claim_successfully() { "whale-uusdc".to_string(), vec![Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1_000u128), + amount: Uint128::from(100_000u128), }], |result| { result.unwrap(); }, ) - .swap( - sender.clone(), - coin(1_000u128, "uwhale"), - "uusdc".to_string(), - None, - None, - None, - "whale-uusdc".to_string(), - vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000u128), - }], - |result| { - result.unwrap(); - }, - ); - - robot.query_claimable_epochs_live(None, |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 1); - }); - robot.query_claimable_epochs_live(Some(sender.clone()), |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 1); - }); + .add_one_day() + // epoch 5 + .create_new_epoch(); - robot.claim(sender.clone(), |res| { - let result = res.unwrap(); - assert!(result.events.iter().any(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "amount" && attr.value == "571uwhale") - })); - }); + suite + .query_claimable_reward_buckets(None, |res| { + let claimable_reward_buckets = res.unwrap().1; - robot.claim(another_sender.clone(), |result| { - let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::NothingToClaim => {} - _ => { - panic!("Wrong error type, should return ContractError::NothingToClaim") - } - } - }); + assert_eq!( + claimable_reward_buckets, + vec![ + 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_700u128), + 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(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)); + }); - robot + suite .bond( another_sender.clone(), Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(9_000u128), + denom: "bWHALE".to_string(), + amount: Uint128::new(1_000u128), }, - &coins(9_000u128, "ampWHALE"), - |res| { - res.unwrap(); + &coins(1_000u128, "bWHALE"), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::UnclaimedRewards { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), + } }, ) - .assert_bonded_response( - another_sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(9_000u128), - bonded_assets: vec![Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(9_000u128), - }], - first_bonded_epoch_id: Uint64::new(3u64), + .bond( + yet_another_sender.clone(), + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(5_000u128), + }, + &coins(5_000u128, "bWHALE"), + |result| { + result.unwrap(); }, ); - robot - .fast_forward(100_000) - .create_epoch(|result| { - result.unwrap(); - println!("*****"); - }) + suite .swap( another_sender.clone(), - coin(1_000u128, "uusdc"), + coin(80_000u128, "uusdc"), "uwhale".to_string(), None, None, @@ -456,63 +549,558 @@ fn test_claim_successfully() { "whale-uusdc".to_string(), vec![Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1_000u128), + amount: Uint128::from(80_000u128), }], |result| { result.unwrap(); }, ) - .swap( - another_sender.clone(), - coin(1_000u128, "uwhale"), - "uusdc".to_string(), - None, - None, - None, - "whale-uusdc".to_string(), - vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000u128), - }], + .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 claim now + + 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 + .bond( + creator.clone(), + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + &coins(1_000u128, "bWHALE"), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::UnclaimedRewards { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), + } + }, + ) + .claim(creator.clone(), |result| { + result.unwrap(); + }) + .bond( + creator.clone(), + Coin { + denom: "bWHALE".to_string(), + amount: Uint128::new(1_000u128), + }, + &coins(1_000u128, "bWHALE"), |result| { result.unwrap(); }, ) - .swap( - sender.clone(), - coin(5_000u128, "uusdc"), + .claim(creator.clone(), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::NothingToClaim { .. } => {} + _ => panic!("Wrong error type, should return ContractError::NothingToClaim"), + } + }) + .claim(another_sender.clone(), |result| { + result.unwrap(); + }) + .claim(yet_another_sender.clone(), |result| { + result.unwrap(); + }); + + suite + .query_balance("uwhale".to_string(), creator.clone(), |balance| { + assert_eq!( + creator_balance.clone().into_inner() + Uint128::new(1078u128), + balance + ); + }) + .query_balance("uwhale".to_string(), another_sender.clone(), |balance| { + assert_eq!( + another_sender_balance.clone().into_inner() + Uint128::new(421u128), + balance + ); + }) + .query_balance( "uwhale".to_string(), - None, - None, - None, - "whale-uusdc".to_string(), - vec![Coin { - denom: "uusdc".to_string(), - amount: Uint128::from(5_000u128), - }], + yet_another_sender.clone(), + |balance| { + assert_eq!( + yet_another_sender_balance.clone().into_inner() + Uint128::new(496u128), + balance + ); + }, + ); + + 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(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(), + }, + }, + ] + ); + }); +} + +#[test] +fn test_rewards_forwarding() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].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() + .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(); }, ) - .swap( - sender.clone(), - coin(5_000u128, "uwhale"), - "uusdc".to_string(), - None, - None, - None, + .provide_liquidity( + creator.clone(), "whale-uusdc".to_string(), - vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(5_000u128), - }], + 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| { - result.unwrap(); + // 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() + }) + })); }, ); - robot.query_claimable_epochs_live(Some(another_sender.clone()), |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 1); + for i in 1..=20 { + suite + .swap( + creator.clone(), + coin(i * 1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(i * 1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(i * 1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(i * 1_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + suite.add_one_day().create_new_epoch(); + } + + suite.query_claimable_reward_buckets(None, |res| { + let (_, claimable_reward_buckets) = res.unwrap(); + assert_eq!(claimable_reward_buckets.len(), 20usize); + + let first_bucket = claimable_reward_buckets.last().unwrap().clone(); + assert_eq!( + first_bucket, + RewardBucket { + id: 1, + epoch_start_time: Timestamp::from_nanos(1571883819879305533), + 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: 1, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 1, + last_weight: Default::default(), + }, + } + ); + + assert_eq!(claimable_reward_buckets[0].id, 20u64); + assert_eq!(claimable_reward_buckets[18].id, 2u64); + assert_eq!( + claimable_reward_buckets[18].available[0].amount, + Uint128::new(19) + ); + }); + + // create two more epochs (without swapping, so they won't have any rewards), so the first bucket is forwarded to the one with id 22 + for i in 1..=2 { + suite.add_one_day().create_new_epoch(); + } + + suite.query_claimable_reward_buckets(None, |res| { + let (_, claimable_reward_buckets) = res.unwrap(); + + assert_eq!( + claimable_reward_buckets[0], + RewardBucket { + id: 22, + epoch_start_time: Timestamp::from_nanos(1573698219879305533), + total: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), // 1_009 forwarded from bucket 1 + }], + available: vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1_009), // 1_009 forwarded from bucket 1 + }], + claimed: vec![], + global_index: GlobalIndex { + epoch_id: 22, + bonded_amount: Default::default(), + bonded_assets: vec![], + last_updated: 22, + last_weight: Default::default(), + }, + } + ); + + assert_eq!( + claimable_reward_buckets[19], + RewardBucket { + id: 2, + epoch_start_time: Timestamp::from_nanos(1571970219879305533), + 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: 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/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index 92ba66f5..fad5aead 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -1,7 +1,7 @@ // mod bond; -// mod claim; +mod claim; mod instantiate; -// mod rewards; +mod rewards; mod suite; // mod unbond; mod update_config; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs index d5243862..0c7084df 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs @@ -6,12 +6,11 @@ use white_whale_std::pool_manager::SwapRoute; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; use crate::tests::suite::TestingSuite; -use crate::tests::test_helpers; #[test] fn test_fill_rewards_from_pool_manager() { - let mut robot = TestingSuite::default(); - let creator = robot.sender.clone(); + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; @@ -31,12 +30,10 @@ fn test_fill_rewards_from_pool_manager() { extra_fees: vec![], }; - robot + suite .instantiate_default() .fast_forward(90_000) - .create_epoch(|result| { - result.unwrap(); - }) + .create_new_epoch() .create_pair( creator.clone(), asset_denoms.clone(), @@ -50,7 +47,7 @@ fn test_fill_rewards_from_pool_manager() { ); // Lets try to add liquidity - robot.provide_liquidity( + suite.provide_liquidity( creator.clone(), "whale-uusdc".to_string(), vec![ @@ -64,15 +61,7 @@ fn test_fill_rewards_from_pool_manager() { }, ], |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() - }) - })); + result.unwrap(); }, ); @@ -86,61 +75,62 @@ fn test_fill_rewards_from_pool_manager() { pool_identifier: "whale-uusdc".to_string(), }], }; - robot.add_swap_routes(creator.clone(), vec![swap_route_1], |res| { - res.unwrap(); - }); - robot.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(); - }, - ); + suite + .add_swap_routes(creator.clone(), vec![swap_route_1], |res| { + res.unwrap(); + }) + .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(); + }, + ); // Get balance of the bonding manager it should have received fees from the swap - robot.query_balance( - "uwhale".to_string(), - robot.bonding_manager_addr.clone(), - |res| { - // 1_000u128 - 9u128 swap_fee - 9u128 protocol_fee where protocol_fee and swap_fee are 1% of the swap amount - // + 1_000u128 uwhale pool creation fee - assert_eq!(res, Uint128::from(1009u128)); - }, - ); - - robot.create_pair( - creator.clone(), - asset_denoms.clone(), - pool_fees.clone(), - white_whale_std::pool_manager::PoolType::ConstantProduct, - Some("whale-uusdc-second".to_string()), - vec![coin(1000, "uwhale")], - |result| { - result.unwrap(); - }, - ); + suite + .query_balance( + "uwhale".to_string(), + suite.bonding_manager_addr.clone(), + |res| { + // 1_000u128 - 9u128 swap_fee - 9u128 protocol_fee where protocol_fee and swap_fee are 1% of the swap amount + // + 1_000u128 uwhale pool creation fee + assert_eq!(res, Uint128::from(1009u128)); + }, + ) + .create_pair( + creator.clone(), + asset_denoms.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc-second".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); + }, + ); // Get balance of the bonding manager again it should have the pool creation fee - robot.query_balance( + suite.query_balance( "uwhale".to_string(), - robot.bonding_manager_addr.clone(), + suite.bonding_manager_addr.clone(), |res| { assert_eq!(res, Uint128::from(2009u128)); }, ); // create another pair to collect another fee - robot.create_pair( + suite.create_pair( creator.clone(), asset_denoms, pool_fees, @@ -152,15 +142,15 @@ fn test_fill_rewards_from_pool_manager() { }, ); // Verify the fee has been collected - robot.query_balance( + suite.query_balance( "uwhale".to_string(), - robot.bonding_manager_addr.clone(), + suite.bonding_manager_addr.clone(), |res| { assert_eq!(res, Uint128::from(3009u128)); }, ); - robot.fill_rewards_lp( + suite.fill_rewards_lp( creator.clone(), vec![coin( 1000, @@ -171,17 +161,21 @@ fn test_fill_rewards_from_pool_manager() { }, ); - let bonding_manager_addr = robot.bonding_manager_addr.clone(); - let bonding_manager_balances = robot + let bonding_manager_addr = suite.bonding_manager_addr.clone(); + let bonding_manager_balances = suite .app .wrap() .query_all_balances(bonding_manager_addr.clone()) .unwrap(); assert_eq!(bonding_manager_balances.len(), 1); - assert_eq!(bonding_manager_balances[0].amount, Uint128::from(4998u128)); + // 4998 = 4989 (3009 + 1980 (from the lp withdrawal swap)) + 9 (fee from the lp withdrawal swap) + assert_eq!( + bonding_manager_balances[0].amount, + Uint128::from(4989u128 + 9u128) + ); // send some random asset that doesn't have swap routes - robot.fill_rewards_lp( + suite.fill_rewards_lp( creator.clone(), vec![coin(1000, "non_whitelisted_asset")], |res| { @@ -189,8 +183,8 @@ fn test_fill_rewards_from_pool_manager() { }, ); - let bonding_manager_addr = robot.bonding_manager_addr.clone(); - let bonding_manager_balances = robot + let bonding_manager_addr = suite.bonding_manager_addr.clone(); + let bonding_manager_balances = suite .app .wrap() .query_all_balances(bonding_manager_addr.clone()) diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index 1849c53e..e50e3603 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -71,8 +71,7 @@ type OsmosisTokenFactoryApp = App< >; pub struct TestingSuite { pub app: OsmosisTokenFactoryApp, - pub sender: Addr, - pub another_sender: Addr, + pub senders: Vec, pub bonding_manager_addr: Addr, pub pool_manager_addr: Addr, pub epoch_manager_addr: Addr, @@ -116,8 +115,7 @@ impl TestingSuite { Self { app: app, - sender, - another_sender, + senders: vec![sender, another_sender, sender_3], bonding_manager_addr: Addr::unchecked(""), pool_manager_addr: Addr::unchecked(""), epoch_manager_addr: Addr::unchecked(""), @@ -167,7 +165,7 @@ impl TestingSuite { .app .instantiate_contract( epoch_manager_id, - self.sender.clone(), + self.senders[0].clone(), &white_whale_std::epoch_manager::epoch_manager::InstantiateMsg { start_epoch: EpochV2 { id: 0, @@ -195,7 +193,7 @@ impl TestingSuite { let resp = self .app .execute_contract( - self.sender.clone(), + self.senders[0].clone(), epoch_manager_addr.clone(), &hook_registration_msg, &[], @@ -213,7 +211,7 @@ impl TestingSuite { let pool_manager_id = self.app.store_code(contract_pool_manager()); - let creator = self.sender.clone(); + let creator = self.senders[0].clone(); let pool_manager_addr = self .app @@ -233,7 +231,12 @@ impl TestingSuite { unbonding_period: None, }; self.app - .execute_contract(self.sender.clone(), bonding_manager_addr.clone(), &msg, &[]) + .execute_contract( + self.senders[0].clone(), + bonding_manager_addr.clone(), + &msg, + &[], + ) .unwrap(); self.bonding_manager_addr = bonding_manager_addr; @@ -332,7 +335,7 @@ impl TestingSuite { let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; self.app .execute_contract( - self.sender.clone(), + self.senders[0].clone(), self.epoch_manager_addr.clone(), &new_epoch_msg, &[], @@ -400,11 +403,11 @@ fn instantiate_contract( let bonding_manager_id = suite.app.store_code(bonding_manager_contract()); suite.app.instantiate_contract( bonding_manager_id, - suite.sender.clone(), + suite.senders[0].clone(), &msg, funds, "Bonding Manager".to_string(), - Some(suite.sender.clone().to_string()), + Some(suite.senders[0].clone().to_string()), ) } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index 7f6dd3e2..0aa82242 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -8,7 +8,7 @@ use crate::ContractError; #[test] fn test_update_config_successfully() { let mut suite = TestingSuite::default(); - let owner = suite.sender.clone(); + let owner = suite.senders[0].clone(); suite .instantiate_default() @@ -85,7 +85,7 @@ fn test_update_config_successfully() { #[test] fn test_update_config_unsuccessfully() { let mut suite = TestingSuite::default(); - let owner = suite.sender.clone(); + let owner = suite.senders[0].clone(); suite .instantiate_default() diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 646022e4..cf11b62e 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -59,7 +59,7 @@ pub struct Bond { /// 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 updated_last: u64, + pub last_updated: u64, /// The weight of the bond at the given block height. pub weight: Uint128, //pub previous: Option<(u64, Uint128)> @@ -73,7 +73,7 @@ impl Default for Bond { amount: Uint128::zero(), }, created_at_epoch: Default::default(), - updated_last: Default::default(), + last_updated: Default::default(), weight: Uint128::zero(), //previous: None, } @@ -90,9 +90,9 @@ pub struct GlobalIndex { /// Assets that are bonded in the contract. pub bonded_assets: Vec, /// The epoch id at which the total bond was updated. - pub updated_last: u64, - /// The total weight of the bond at the given block height. - pub weight: Uint128, + pub last_updated: u64, + /// The total weight of the contract at the given updated_last epoch id. + pub last_weight: Uint128, } #[cw_serde] From bcb76767e1b10d972fd1d1934a85d7f978292b57 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 16 May 2024 12:54:18 +0100 Subject: [PATCH 32/51] test: add more test coverage, to helpers and bonding --- .../bonding-manager/src/helpers.rs | 2 +- .../bonding-manager/src/tests/bond.rs | 365 ++---------------- .../bonding-manager/src/tests/claim.rs | 72 +--- .../bonding-manager/src/tests/epoch.rs | 39 +- .../bonding-manager/src/tests/helpers.rs | 37 ++ .../bonding-manager/src/tests/mod.rs | 4 +- .../bonding-manager/src/tests/rewards.rs | 65 +++- .../bonding-manager/src/tests/suite.rs | 25 +- 8 files changed, 186 insertions(+), 423 deletions(-) create mode 100644 contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 46fb04ec..7e6390e7 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -144,7 +144,7 @@ pub fn handle_lp_tokens_rewards( /// Extracts the pool identifier from an LP token denom. /// LP tokens have the format "{pair_label}.pool.{identifier}.{LP_SYMBOL}", get the /// identifier and not the LP SYMBOL. The identifier can contain dots, slashes, etc. -fn extract_pool_identifier(lp_token_denom: &str) -> Option<&str> { +pub(crate) fn extract_pool_identifier(lp_token_denom: &str) -> Option<&str> { // Split the string at ".pool." to isolate the part after ".pool." let parts: Vec<&str> = lp_token_denom.splitn(2, ".pool.").collect(); if parts.len() < 2 { diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index 33fae6db..3579dd79 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -1,359 +1,58 @@ -use cosmwasm_std::{coins, Coin, Decimal, Uint128}; +use cosmwasm_std::{coin, coins, Coin, Decimal, Timestamp, Uint128}; use std::cell::RefCell; -use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse, GlobalIndex}; +use white_whale_std::bonding_manager::{ + BondedResponse, BondingWeightResponse, GlobalIndex, RewardBucket, +}; +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_bond_successfully() { +fn test_bond_unsuccessful() { let mut suite = TestingSuite::default(); - let sender = suite.sender.clone(); - let another_sender = suite.another_sender.clone(); - - let global_index = RefCell::new(GlobalIndex::default()); + let creator = suite.senders[0].clone(); suite .instantiate_default() + .bond(creator.clone(), &[], |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::PaymentError(error) => { + assert_eq!(error, cw_utils::PaymentError::NoFunds {}) + } + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }) .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), + creator.clone(), + &coins(1_000u128, "non_whitelisted_asset"), |result| { let err = result.unwrap_err().downcast::().unwrap(); + match err { - // no epochs has been created yet - ContractError::Unauthorized => {} - _ => { - panic!("Wrong error type, should return ContractError::Unauthorized") + ContractError::PaymentError(error) => { + assert_eq!(error, cw_utils::PaymentError::NoFunds {}) } + _ => panic!("Wrong error type, should return PaymentError::NoFunds"), } }, ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Default::default(), - bonded_assets: Default::default(), - first_bonded_epoch_id: None, - }, - ); - - suite - .add_one_day() - // created epoch 1 - .create_new_epoch() - .query_global_index(Some(1u64), |res| { - let gi = res.unwrap().1; - *global_index.borrow_mut() = gi.clone(); - println!("gi 1:: {:?}", gi); - }) .bond( - sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), + creator.clone(), + &vec![coin(1_000u128, "bWHALE"), coin(1_000u128, "ampWHALE")], |result| { - result.unwrap(); - }, - ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(1_000u128), - bonded_assets: vec![Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }], - first_bonded_epoch_id: Some(1u64), - }, - ) - .assert_bonding_weight_response( - sender.to_string(), - Some(1u64), - Some(global_index.clone().into_inner()), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::zero(), - global_weight: Uint128::zero(), // because the snapshot was taken at the beginning of the epoch - share: Decimal::zero(), - epoch_id: 1u64, - }, - ); - - suite - .fast_forward(43_200u64) - .bond( - sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), - }, - &coins(3_000u128, "bWHALE"), - |result| { - result.unwrap(); - }, - ) - .assert_bonded_response( - sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(4_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(3_000u128), - }, - ], - first_bonded_epoch_id: Some(1u64), - }, - ); - - suite - .add_one_day() - // epoch 2 - .create_new_epoch() - .query_global_index(Some(2u64), |res| { - let gi = res.unwrap().1; - println!("gi 2:: {:?}", gi); - *global_index.borrow_mut() = gi.clone(); // 8000 weight - }); - - suite - .query_weight( - sender.to_string(), - Some(2u64), - Some(global_index.clone().into_inner()), - |res| { - let bonded_response = res.unwrap().1; - println!("bonded_response 1:: {:?}", bonded_response); - }, - ) - .bond( - sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "bWHALE"), - |result| { - result.unwrap(); - }, - ) - .assert_bonded_response( - sender.to_string(), // weight -> 10k - BondedResponse { - total_bonded: Uint128::new(5_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(4_000u128), - }, - ], - first_bonded_epoch_id: Some(1u64), - }, - ); - - println!( - "herrreeee global_index:: {:?}", - global_index.clone().into_inner() - ); - - suite - .assert_bonding_weight_response( - sender.to_string(), - Some(2u64), - None, - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(6_000u128), - global_weight: Uint128::new(8_000u128), - share: Decimal::from_ratio(6_000u128, 8_000u128), - epoch_id: 2u64, - }, - ) - .query_weight(sender.to_string(), Some(2u64), None, |res| { - let bonded_response = res.unwrap().1; - println!("bonded_response 2:: {:?}", bonded_response); - }); + let err = result.unwrap_err().downcast::().unwrap(); - suite - .query_bonded(None, |res| { - let bonded_response = res.unwrap().1; - assert_eq!( - bonded_response, - BondedResponse { - total_bonded: Uint128::new(5_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(4_000u128), - }, - ], - first_bonded_epoch_id: None, - } - ) - }) - .bond( - another_sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(5_000u128), - }, - &coins(5_000u128, "ampWHALE"), - |_res| {}, - ) - .assert_bonded_response( - another_sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(5_000u128), - bonded_assets: vec![Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(5_000u128), - }], - first_bonded_epoch_id: Some(2u64), - }, - ) - .assert_bonding_weight_response( - another_sender.to_string(), - Some(2u64), - Some(global_index.clone().into_inner()), - BondingWeightResponse { - address: another_sender.to_string(), - weight: Uint128::new(5_000u128), - global_weight: Uint128::new(15_000u128), - share: Decimal::from_ratio(5_000u128, 15_000u128), - epoch_id: 2u64, - }, - ) - .query_bonded(None, |res| { - let bonded_response = res.unwrap().1; - assert_eq!( - bonded_response, - BondedResponse { - total_bonded: Uint128::new(10_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(6_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(4_000u128), - }, - ], - first_bonded_epoch_id: None, + match err { + ContractError::PaymentError(error) => { + assert_eq!(error, cw_utils::PaymentError::MultipleDenoms {}) + } + _ => panic!("Wrong error type, should return PaymentError::MultipleDenoms"), } - ) - }) - .query_weight(sender.to_string(), Some(2u64), None, |res| { - let bonded_response = res.unwrap().1; - println!("bonded_response sender:: {:?}", bonded_response); - }) - .query_weight(another_sender.to_string(), Some(2u64), None, |res| { - let bonded_response = res.unwrap().1; - println!("bonded_response another_sender:: {:?}", bonded_response); - }); - - suite - .add_one_day() - .create_new_epoch() - .query_global_index(Some(3u64), |res| { - let gi = res.unwrap().1; - *global_index.borrow_mut() = gi.clone(); - println!("gi:: {:?}", gi); - }) - .query_weight(sender.to_string(), Some(3u64), None, |res| { - let bonded_response = res.unwrap().1; - println!("bonded_response sender again:: {:?}", bonded_response); - }) - .query_weight(another_sender.to_string(), Some(3u64), None, |res| { - let bonded_response = res.unwrap().1; - println!( - "bonded_response another_sender again:: {:?}", - bonded_response - ); - }); - - suite.assert_bonding_weight_response( - another_sender.to_string(), - Some(3u64), - Some(global_index.clone().into_inner()), - BondingWeightResponse { - address: another_sender.to_string(), - weight: Uint128::new(10_000u128), - global_weight: Uint128::new(25_000u128), - share: Decimal::from_ratio(10_000u128, 25_000u128), - epoch_id: 3u64, - }, - ); - - suite - .bond( - another_sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(2_000u128), - }, - &coins(2_000u128, "bWHALE"), - |_res| {}, - ) - .assert_bonded_response( - another_sender.to_string(), - BondedResponse { - total_bonded: Uint128::new(7_000u128), - bonded_assets: vec![ - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(5_000u128), - }, - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(2_000u128), - }, - ], - first_bonded_epoch_id: Some(2u64), - }, - ) - .assert_bonding_weight_response( - another_sender.to_string(), - Some(3u64), - Some(global_index.clone().into_inner()), - BondingWeightResponse { - address: another_sender.to_string(), - weight: Uint128::new(12_000u128), - global_weight: Uint128::new(29_000u128), - share: Decimal::from_ratio(12_000u128, 29_000u128), - epoch_id: 3u64, - }, - ) - .assert_bonding_weight_response( - sender.to_string(), - Some(3u64), - Some(global_index.clone().into_inner()), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(15_000u128), - global_weight: Uint128::new(29_000u128), - share: Decimal::from_ratio(15_000u128, 29_000u128), - 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 91654890..841b99f3 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -224,17 +224,9 @@ fn test_claim_successfully() { // we bond tokens with the creator suite - .bond( - creator.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "ampWHALE"), - |res| { - res.unwrap(); - }, - ) + .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 @@ -357,17 +349,9 @@ fn test_claim_successfully() { suite .fast_forward(20_000) - .bond( - another_sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(700u128), - }, - &coins(700u128, "bWHALE"), - |res| { - res.unwrap(); - }, - ) + .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 @@ -512,10 +496,6 @@ fn test_claim_successfully() { suite .bond( another_sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, &coins(1_000u128, "bWHALE"), |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -528,10 +508,6 @@ fn test_claim_successfully() { ) .bond( yet_another_sender.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(5_000u128), - }, &coins(5_000u128, "bWHALE"), |result| { result.unwrap(); @@ -728,36 +704,20 @@ fn test_claim_successfully() { ); suite - .bond( - creator.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "bWHALE"), - |result| { - let err = result.unwrap_err().downcast::().unwrap(); + .bond(creator.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"), - } - }, - ) + match err { + ContractError::UnclaimedRewards { .. } => {} + _ => panic!("Wrong error type, should return ContractError::UnclaimedRewards"), + } + }) .claim(creator.clone(), |result| { result.unwrap(); }) - .bond( - creator.clone(), - Coin { - denom: "bWHALE".to_string(), - amount: Uint128::new(1_000u128), - }, - &coins(1_000u128, "bWHALE"), - |result| { - result.unwrap(); - }, - ) + .bond(creator.clone(), &coins(1_000u128, "bWHALE"), |result| { + result.unwrap(); + }) .claim(creator.clone(), |result| { let err = result.unwrap_err().downcast::().unwrap(); diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/epoch.rs b/contracts/liquidity_hub/bonding-manager/src/tests/epoch.rs index ae1a31ee..a87c9453 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/epoch.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/epoch.rs @@ -1,35 +1,20 @@ -use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{Timestamp, Uint64}; -use white_whale_std::epoch_manager::epoch_manager::EpochConfig; - use crate::ContractError; -use white_whale_std::bonding_manager::Epoch; -use white_whale_std::pool_network::asset::AssetInfo; -use crate::tests::robot::TestingRobot; -use crate::tests::test_helpers; +use crate::tests::suite::TestingSuite; #[test] -fn test_current_epoch_no_epochs() { - let mut robot = TestingRobot::default(); +fn test_call_on_epoch_created_hook_unauthorized() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); - robot - .instantiate_default() - .assert_current_epoch(&Epoch::default()) - .query_epoch(Uint64::new(10), |res| { - // epoch 10 doesn't exist, it should return the default value - let (_, epoch) = res.unwrap(); - assert_eq!(epoch, Epoch::default()); - }); -} + suite.instantiate_default().add_one_day().create_new_epoch(); -#[test] -fn test_expiring_epoch() { - let mut robot = TestingRobot::default(); - let epochs = test_helpers::get_epochs(); + suite.on_epoch_created(creator, |result| { + let err = result.unwrap_err().downcast::().unwrap(); - robot - .instantiate_default() - // .add_epochs_to_state(epochs.clone()) - .assert_expiring_epoch(Some(&epochs[1])); + match err { + ContractError::Unauthorized { .. } => {} + _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + } + }); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs new file mode 100644 index 00000000..bc3092aa --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs @@ -0,0 +1,37 @@ +use crate::helpers::extract_pool_identifier; +use cosmwasm_std::coin; + +#[test] +fn test_extract_pool_identifier() { + let denom_1 = "non_whitelisted_asset"; + let denom_2 = "factory/contract100/uluna-uwhale.pool.random_identifier.uLP"; + let denom_3 = "factory/contract100/uluna-uwhale.pool..pool./.pool.crazy.pool.identifier.uLP"; + let denom_4 = "factory/contract100/uluna-uwhale.pool.messy_.pool._identifier.uLP"; + let denom_5 = "factory/contract100/uluna-uwhale.pool./hacky_.pool./_identifier.uLP"; + let denom_6 = "factory/contract100/uluna-uwhale.pair.1.uLP"; + let denom_7 = "factory/contract100/uluna-uwhale.pair.1"; + let denom_8 = "factory/contract100/bWHALE"; + + let res = extract_pool_identifier(denom_1); + assert!(res.is_none()); + + let res = extract_pool_identifier(denom_2); + assert_eq!(res.unwrap(), "random_identifier"); + let res = extract_pool_identifier(denom_3); + assert_eq!(res.unwrap(), ".pool./.pool.crazy.pool.identifier"); + + let res = extract_pool_identifier(denom_4); + assert_eq!(res.unwrap(), "messy_.pool._identifier"); + + let res = extract_pool_identifier(denom_5); + assert_eq!(res.unwrap(), "/hacky_.pool./_identifier"); + + let res = extract_pool_identifier(denom_6); + assert!(res.is_none()); + + let res = extract_pool_identifier(denom_7); + assert!(res.is_none()); + + let res = extract_pool_identifier(denom_8); + assert!(res.is_none()); +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs index fad5aead..1139d140 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/mod.rs @@ -1,8 +1,10 @@ -// mod bond; +mod bond; mod claim; +mod epoch; mod instantiate; mod rewards; mod suite; // mod unbond; +mod helpers; mod update_config; // mod withdraw; diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs index 0c7084df..40d41b85 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs @@ -1,9 +1,9 @@ use std::vec; use cosmwasm_std::{coin, Coin, Decimal, Uint128}; + use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_manager::SwapRoute; -use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; use crate::tests::suite::TestingSuite; @@ -150,7 +150,7 @@ fn test_fill_rewards_from_pool_manager() { }, ); - suite.fill_rewards_lp( + suite.fill_rewards( creator.clone(), vec![coin( 1000, @@ -175,7 +175,7 @@ fn test_fill_rewards_from_pool_manager() { ); // send some random asset that doesn't have swap routes - suite.fill_rewards_lp( + suite.fill_rewards( creator.clone(), vec![coin(1000, "non_whitelisted_asset")], |res| { @@ -205,3 +205,62 @@ fn test_fill_rewards_from_pool_manager() { ] ); } + +#[test] +fn test_fill_random_lp_token() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); + + suite + .instantiate_default() + .fast_forward(90_000) + .create_new_epoch(); + + // Get balance of the bonding manager it should have received fees from the swap + suite.query_balance( + "uwhale".to_string(), + suite.bonding_manager_addr.clone(), + |res| { + assert_eq!(res, Uint128::zero()); + }, + ); + + suite.fill_rewards(creator.clone(), vec![coin(5_000, "uwhale")], |res| { + res.unwrap(); + }); + + suite.query_balance( + "uwhale".to_string(), + suite.bonding_manager_addr.clone(), + |res| { + assert_eq!(res, Uint128::new(5_000)); + }, + ); + + // this token doesn't exist in the pool manager. It shouldn't be swapped + suite.fill_rewards( + creator.clone(), + vec![coin( + 1000, + "factory/contract100/uluna-uwhale.pool.random_identifier.uLP", + )], + |res| { + res.unwrap(); + }, + ); + + suite.query_balance( + "uwhale".to_string(), + suite.bonding_manager_addr.clone(), + |res| { + assert_eq!(res, Uint128::new(5_000)); + }, + ); + suite.query_balance( + "factory/contract100/uluna-uwhale.pool.random_identifier.uLP".to_string(), + suite.bonding_manager_addr.clone(), + |res| { + assert_eq!(res, Uint128::new(1_000)); + }, + ); +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs index e50e3603..4158ee63 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/suite.rs @@ -94,6 +94,10 @@ impl TestingSuite { coin(1_000_000_000_000, "ampWHALE"), coin(1_000_000_000_000, "bWHALE"), coin(1_000_000_000_000, "non_whitelisted_asset"), + coin( + 1_000_000_000_000, + "factory/contract100/uluna-uwhale.pool.random_identifier.uLP", + ), ]; let balances = vec![ @@ -266,7 +270,6 @@ impl TestingSuite { pub(crate) fn bond( &mut self, sender: Addr, - _asset: Coin, funds: &[Coin], response: impl Fn(Result), ) -> &mut Self { @@ -345,6 +348,24 @@ impl TestingSuite { self } + #[track_caller] + pub(crate) fn on_epoch_created( + &mut self, + sender: Addr, + response: impl Fn(Result), + ) -> &mut Self { + let msg = ExecuteMsg::EpochChangedHook { + current_epoch: Default::default(), + }; + + response( + self.app + .execute_contract(sender, self.bonding_manager_addr.clone(), &msg, &[]), + ); + + self + } + #[track_caller] pub(crate) fn query_balance( &mut self, @@ -644,7 +665,7 @@ impl TestingSuite { } #[track_caller] - pub(crate) fn fill_rewards_lp( + pub(crate) fn fill_rewards( &mut self, sender: Addr, funds: Vec, From 0e8c4e546bbd7b687879aee8ca2abe318b47ca97 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 16 May 2024 17:05:26 +0100 Subject: [PATCH 33/51] 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. From 4b8f8401466efd7570a8651b2aa4fc2e054e792d Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Thu, 16 May 2024 17:21:50 +0100 Subject: [PATCH 34/51] chore: fix broken tests --- .../epoch-manager/src/commands.rs | 1 - .../src/tests/integration_tests.rs | 46 +++++++------- .../pool-manager/src/tests/suite.rs | 63 ++----------------- .../pool-network/incentive/src/tests/suite.rs | 1 - 4 files changed, 29 insertions(+), 82 deletions(-) diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index 93be93e2..85c23c39 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -32,7 +32,6 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= current_epoch.start_time, diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index fc37dfb3..b4887d3a 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -2701,7 +2701,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2753,7 +2753,7 @@ mod locking_lp { assert_eq!(positions.len(), 2); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2761,7 +2761,7 @@ mod locking_lp { }); assert_eq!(positions[1], Position { identifier: "2".to_string(), - lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, unlocking_duration: 200_000, open: true, expiring_at: None, @@ -2897,7 +2897,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2950,7 +2950,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin { denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -4271,15 +4271,15 @@ mod multiple_pools { balances, vec![ Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { @@ -4326,7 +4326,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1001000, "uwhale"), coin(999070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4369,7 +4369,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_140, "uwhale"), coin(1_001_070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4422,7 +4422,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1001000, "uwhale"), coin(999_150, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4458,7 +4458,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_300, "uwhale"), coin(1_001_150, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4512,7 +4512,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1003000, "uluna"), coin(997_218, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4553,7 +4553,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_001_599, "uluna"), coin(998_718, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4592,15 +4592,15 @@ mod multiple_pools { let balances = result.unwrap(); assert_eq!(balances, vec![ Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { @@ -4650,7 +4650,7 @@ mod multiple_pools { // this should have not changed since last time, since we didn't touch this pool assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(999_140, "uwhale"), coin(1_001_070, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4668,7 +4668,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uwhale".to_string(), "uluna".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_004_300, "uwhale"), coin(996_913, "uluna")], pool_type: PoolType::ConstantProduct, @@ -4685,7 +4685,7 @@ mod multiple_pools { assert_eq!(pool_info, PoolInfo { asset_denoms: vec!["uluna".to_string(), "uusd".to_string()], - lp_denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + lp_denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), asset_decimals: vec![6u8, 6u8], assets: vec![coin(1_005_587, "uluna"), coin(995_035, "uusd")], pool_type: PoolType::ConstantProduct, @@ -4720,15 +4720,15 @@ mod multiple_pools { let balances = result.unwrap(); assert_eq!(balances, vec![ Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uluna-uusd.pool.uluna-uusd-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-1.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { - denom: "factory/migaloo16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580ws2xujnh/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), + denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna-pool-2.uLP".to_string(), amount: Uint128::from(1_000u128), }, Coin { diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index ea7c6fce..8c8aecaf 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -189,9 +189,10 @@ impl TestingSuite { if !bonding_manager_addr.into_string().is_empty() { let pool_manager_addr = self.pool_manager_addr.clone(); + let epoch_manager_addr = self.epoch_manager_addr.clone(); let msg = white_whale_std::bonding_manager::ExecuteMsg::UpdateConfig { - owner: None, + epoch_manager_addr: Some(epoch_manager_addr.into_string()), pool_manager_addr: Some(pool_manager_addr.into_string()), unbonding_period: None, growth_rate: None, @@ -212,8 +213,8 @@ impl TestingSuite { #[track_caller] pub(crate) fn instantiate_default(&mut self) -> &mut Self { - self.create_bonding_manager(); self.create_epoch_manager(); + self.create_bonding_manager(); self.create_incentive_manager(); self.add_hook(self.incentive_manager_addr.clone()); self.add_hook(self.bonding_manager_addr.clone()); @@ -230,13 +231,15 @@ impl TestingSuite { fn create_bonding_manager(&mut self) { let bonding_manager_id = self.app.store_code(bonding_manager_contract()); + let epoch_manager_addr = self.epoch_manager_addr.to_string(); let msg = white_whale_std::bonding_manager::InstantiateMsg { distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(86400u64), + unbonding_period: 86_400u64, growth_rate: Decimal::one(), bonding_assets: vec!["bWHALE".to_string(), "ampWHALE".to_string()], grace_period: Default::default(), + epoch_manager_addr, }; let creator = self.creator().clone(); @@ -283,8 +286,6 @@ impl TestingSuite { } fn add_hook(&mut self, contract: Addr) { - let epoch_manager_id = self.app.store_code(epoch_manager_contract()); - let msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::AddHook { contract_addr: contract.to_string(), }; @@ -530,58 +531,6 @@ impl TestingSuite { self } - /// Adds swap routes to the pool manager contract. - #[track_caller] - pub(crate) fn add_swap_routes_default( - &mut self, - sender: Addr, - result: impl Fn(Result), - ) -> &mut Self { - let swap_routes = vec![ - SwapRoute { - offer_asset_denom: "uluna".to_string(), - ask_asset_denom: "uwhale".to_string(), - swap_operations: vec![ - SwapOperation::WhaleSwap { - token_in_denom: "uwhale".to_string(), - token_out_denom: "uluna".to_string(), - pool_identifier: "whale-uluna".to_string(), - }, - SwapOperation::WhaleSwap { - token_in_denom: "uluna".to_string(), - token_out_denom: "uusd".to_string(), - pool_identifier: "uluna-uusd".to_string(), - }, - ], - }, - SwapRoute { - offer_asset_denom: "uwhale".to_string(), - ask_asset_denom: "uusd".to_string(), - swap_operations: vec![ - SwapOperation::WhaleSwap { - token_in_denom: "uwhale".to_string(), - token_out_denom: "uluna".to_string(), - pool_identifier: "whale-uluna".to_string(), - }, - SwapOperation::WhaleSwap { - token_in_denom: "uluna".to_string(), - token_out_denom: "uusd".to_string(), - pool_identifier: "uluna-uusd".to_string(), - }, - ], - }, - ]; - - result(self.app.execute_contract( - sender, - self.pool_manager_addr.clone(), - &white_whale_std::pool_manager::ExecuteMsg::AddSwapRoutes { swap_routes }, - &[], - )); - - self - } - /// Removes swap routes from the pool manager contract. #[track_caller] pub(crate) fn remove_swap_routes( diff --git a/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs b/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs index 25fcc47d..509e3593 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs @@ -474,7 +474,6 @@ impl TestingSuite { result: impl Fn(Result), ) -> &mut Self { let msg = white_whale_std::pool_network::incentive::ExecuteMsg::Claim {}; - println!("-------------- claiming {}", sender); result(self.app.execute_contract(sender, incentive_addr, &msg, &[])); self From 9b124e394d6779861ba0705c50c6a73843fcb525 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 17 May 2024 13:11:17 +0100 Subject: [PATCH 35/51] refactor: move a few functiosn to helpers.rs --- .../bonding-manager/src/contract.rs | 25 ---- .../bonding-manager/src/helpers.rs | 135 ++++++++++++++++-- .../bonding-manager/src/queries.rs | 126 ++-------------- .../bonding-manager/src/rewards/commands.rs | 7 +- .../bonding-manager/src/state.rs | 13 +- .../white-whale-std/src/bonding_manager.rs | 24 +--- 6 files changed, 139 insertions(+), 191 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 4afb321c..335ea793 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -133,31 +133,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary( &queries::query_withdrawable(deps, address, denom)?, )?), - // QueryMsg::Weight { - // address, - // epoch_id, - // global_index, - // } => { - // let epoch_id = if let Some(epoch_id) = epoch_id { - // epoch_id - // } else { - // // If epoch_id is not provided, use current epoch - // 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, - // &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, - // )?; - // current_epoch.epoch.id - // }; - // - // Ok(to_json_binary(&queries::query_weight( - // deps, - // epoch_id, - // address, - // global_index, - // )?)?) - // } QueryMsg::GlobalIndex { reward_bucket_id } => Ok(to_json_binary( &queries::query_global_index(deps, reward_bucket_id)?, )?), diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index e1513a14..d0789534 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -1,23 +1,26 @@ -use std::collections::HashMap; +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, WasmMsg, + Order, ReplyOn, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; -use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, Config}; +use white_whale_std::bonding_manager::{ + ClaimableRewardBucketsResponse, Config, GlobalIndex, RewardBucket, +}; use white_whale_std::constants::LP_SYMBOL; use white_whale_std::epoch_manager::epoch_manager::EpochResponse; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, }; +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::error::ContractError; -use crate::queries::{query_claimable, query_weight}; -use crate::state::{CONFIG, REWARD_BUCKETS}; +use crate::queries::{query_claimable, MAX_PAGE_LIMIT}; +use crate::state::{get_weight, BOND, CONFIG, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET}; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { @@ -296,7 +299,7 @@ pub fn calculate_rewards( let mut modified_reward_buckets = HashMap::new(); for reward_bucket in claimable_reward_buckets_for_user { - let bonding_weight_response_for_epoch = query_weight( + let user_share = get_user_share( deps, reward_bucket.id, address.to_string(), @@ -304,22 +307,20 @@ pub fn calculate_rewards( )?; // sanity check, if the user has no share in the bucket, skip it - if bonding_weight_response_for_epoch.share.is_zero() { + if user_share.is_zero() { continue; }; // sanity check ensure!( - bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), + user_share <= Decimal::percent(100u64), ContractError::InvalidShare ); let mut claimed_rewards_from_bucket = vec![]; for reward in reward_bucket.total.iter() { - let user_reward = reward - .amount - .checked_mul_floor(bonding_weight_response_for_epoch.share)?; + let user_reward = reward.amount.checked_mul_floor(user_share)?; // make sure the reward is sound let reward_validation: Result<(), StdError> = reward_bucket @@ -368,3 +369,115 @@ pub fn calculate_rewards( } Ok((total_claimable_rewards, attributes, modified_reward_buckets)) } + +/// Gets the user share for the given epoch and global index. +pub(crate) fn get_user_share( + deps: &Deps, + epoch_id: u64, + address: String, + mut global_index: GlobalIndex, +) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let bonds: StdResult> = BOND + .prefix(&address) + .range(deps.storage, None, None, Order::Ascending) + .take(MAX_PAGE_LIMIT as usize) + .collect(); + + let config = CONFIG.load(deps.storage)?; + + let mut total_bond_weight = Uint128::zero(); + + for (_, mut bond) in bonds? { + bond.weight = get_weight( + epoch_id, + bond.weight, + bond.asset.amount, + config.growth_rate, + bond.last_updated, + )?; + + // 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)?; + } + + global_index.last_weight = get_weight( + epoch_id, + global_index.last_weight, + global_index.bonded_amount, + config.growth_rate, + global_index.last_updated, + )?; + + // 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() { + Decimal::zero() + } else { + Decimal::from_ratio(total_bond_weight, global_index.last_weight) + }; + + Ok(share) +} + +/// Returns the reward bucket that is falling out the grace period, which is the one expiring +/// after creating a new epoch is created. +pub fn get_expiring_reward_bucket(deps: Deps) -> Result, ContractError> { + let config = CONFIG.load(deps.storage)?; + let grace_period = config.grace_period; + + // Take grace_period + let buckets = REWARD_BUCKETS + .range(deps.storage, None, None, Order::Descending) + .take(grace_period as usize) + .map(|item| { + let (_, bucket) = item?; + Ok(bucket) + }) + .collect::>>()?; + + // if the buckets vector's length is the same as the grace period it means there is one bucket that + // is expiring once the new one is created i.e. the last bucket in the vector + if buckets.len() == grace_period as usize { + let expiring_reward_bucket: RewardBucket = buckets.into_iter().last().unwrap_or_default(); + Ok(Some(expiring_reward_bucket)) + } else { + // nothing is expiring yet + Ok(None) + } +} + +/// Returns the buckets that are within the grace period, i.e. the ones which fees can still be claimed. +/// The result is ordered by bucket id, descending. Thus, the first element is the current bucket. +pub fn get_claimable_reward_buckets(deps: &Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let grace_period = config.grace_period; + + let mut reward_buckets = REWARD_BUCKETS + .range(deps.storage, None, None, Order::Descending) + .take(grace_period as usize) + .map(|item| { + let (_, bucket) = item?; + + Ok(bucket) + }) + .collect::>>()?; + + reward_buckets.retain(|bucket| !bucket.available.is_empty()); + + Ok(ClaimableRewardBucketsResponse { + reward_buckets: reward_buckets.into(), + }) +} + +/// Fills the upcoming reward bucket with the given funds. +pub fn fill_upcoming_reward_bucket(deps: DepsMut, funds: Coin) -> StdResult<()> { + UPCOMING_REWARD_BUCKET.update(deps.storage, |mut upcoming_bucket| -> StdResult<_> { + upcoming_bucket.total = asset::aggregate_coins(&upcoming_bucket.total, &vec![funds])?; + Ok(upcoming_bucket) + })?; + + Ok(()) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index c8a595f0..2aab70ee 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,18 +1,15 @@ -use std::collections::VecDeque; - -use cosmwasm_std::{Decimal, Deps, Order, StdResult, Uint128}; +use cosmwasm_std::{Deps, Order, StdResult, Uint128}; use cw_storage_plus::Bound; use crate::{helpers, ContractError}; +use white_whale_std::bonding_manager::ClaimableRewardBucketsResponse; use white_whale_std::bonding_manager::{ - Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, RewardsResponse, - UnbondingResponse, WithdrawableResponse, + Bond, BondedResponse, Config, GlobalIndex, RewardsResponse, UnbondingResponse, + WithdrawableResponse, }; -use white_whale_std::bonding_manager::{ClaimableRewardBucketsResponse, RewardBucket}; use crate::state::{ - get_weight, BOND, BONDING_ASSETS_LIMIT, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, - UNBOND, + BOND, BONDING_ASSETS_LIMIT, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, UNBOND, }; /// Queries the current configuration of the contract. @@ -146,65 +143,8 @@ pub(crate) fn query_withdrawable( }) } -/// Queries the current weight of the given address. -pub(crate) fn query_weight( - deps: &Deps, - epoch_id: u64, - address: String, - mut global_index: GlobalIndex, -) -> StdResult { - let address = deps.api.addr_validate(&address)?; - - let bonds: StdResult> = BOND - .prefix(&address) - .range(deps.storage, None, None, Order::Ascending) - .take(MAX_PAGE_LIMIT as usize) - .collect(); - - let config = CONFIG.load(deps.storage)?; - - let mut total_bond_weight = Uint128::zero(); - - for (_, mut bond) in bonds? { - bond.weight = get_weight( - epoch_id, - bond.weight, - bond.asset.amount, - config.growth_rate, - bond.last_updated, - )?; - - // 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)?; - } - - global_index.last_weight = get_weight( - epoch_id, - global_index.last_weight, - global_index.bonded_amount, - config.growth_rate, - global_index.last_updated, - )?; - - // 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() { - Decimal::zero() - } else { - Decimal::from_ratio(total_bond_weight, global_index.last_weight) - }; - - Ok(BondingWeightResponse { - address: address.to_string(), - weight: total_bond_weight, - global_weight: global_index.last_weight, - share, - epoch_id, - }) -} - -/// Queries the global index +/// Queries the global index. If a reward_bucket_id is provided, returns the global index of that reward bucket. +/// Otherwise, returns the current 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 { @@ -220,63 +160,13 @@ pub fn query_global_index(deps: Deps, reward_bucket_id: Option) -> StdResul Ok(global_index) } -/// Returns the reward bucket that is falling out the grace period, which is the one expiring -/// after creating a new epoch is created. -pub fn get_expiring_reward_bucket(deps: Deps) -> Result, ContractError> { - let config = CONFIG.load(deps.storage)?; - let grace_period = config.grace_period; - - // Take grace_period - let buckets = REWARD_BUCKETS - .range(deps.storage, None, None, Order::Descending) - .take(grace_period as usize) - .map(|item| { - let (_, bucket) = item?; - Ok(bucket) - }) - .collect::>>()?; - - // if the buckets vector's length is the same as the grace period it means there is one bucket that - // is expiring once the new one is created i.e. the last bucket in the vector - if buckets.len() == grace_period as usize { - let expiring_reward_bucket: RewardBucket = buckets.into_iter().last().unwrap_or_default(); - Ok(Some(expiring_reward_bucket)) - } else { - // nothing is expiring yet - Ok(None) - } -} - -/// Returns the buckets that are within the grace period, i.e. the ones which fees can still be claimed. -/// The result is ordered by bucket id, descending. Thus, the first element is the current bucket. -pub fn get_claimable_reward_buckets(deps: &Deps) -> StdResult { - let config = CONFIG.load(deps.storage)?; - let grace_period = config.grace_period; - - let mut reward_buckets = REWARD_BUCKETS - .range(deps.storage, None, None, Order::Descending) - .take(grace_period as usize) - .map(|item| { - let (_, bucket) = item?; - - Ok(bucket) - }) - .collect::>>()?; - - reward_buckets.retain(|bucket| !bucket.available.is_empty()); - - Ok(ClaimableRewardBucketsResponse { - reward_buckets: reward_buckets.into(), - }) -} - /// Returns the reward buckets that can be claimed by the given address. If no address is provided, /// returns all possible buckets stored in the contract that can potentially be claimed. pub fn query_claimable( deps: &Deps, address: Option, ) -> StdResult { - let mut claimable_reward_buckets = get_claimable_reward_buckets(deps)?.reward_buckets; + let mut claimable_reward_buckets = helpers::get_claimable_reward_buckets(deps)?.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)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs index 444c2f36..ae0ad7f9 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -8,11 +8,8 @@ use white_whale_std::bonding_manager::{GlobalIndex, RewardBucket, UpcomingReward use white_whale_std::epoch_manager::epoch_manager::Epoch; use white_whale_std::pool_network::asset; -use crate::queries::get_expiring_reward_bucket; -use crate::state::{ - fill_upcoming_reward_bucket, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, - UPCOMING_REWARD_BUCKET, -}; +use crate::helpers::{fill_upcoming_reward_bucket, get_expiring_reward_bucket}; +use crate::state::{CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET}; use crate::{helpers, ContractError}; /// Handles the new epoch created by the epoch manager. It creates a new reward bucket with the diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 07e96b4a..d5102535 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,10 +1,9 @@ use crate::ContractError; -use cosmwasm_std::{Addr, Coin, Decimal, DepsMut, StdError, StdResult, Uint128}; +use cosmwasm_std::{Addr, Decimal, DepsMut, StdError, StdResult, Uint128}; use cw_storage_plus::{Item, Map}; use white_whale_std::bonding_manager::{ Bond, Config, GlobalIndex, RewardBucket, UpcomingRewardBucket, }; -use white_whale_std::pool_network::asset; type Denom = str; @@ -86,13 +85,3 @@ pub fn get_weight( Ok(weight.checked_add(amount.checked_mul(time_factor)? * growth_rate)?) } - -/// Fills the upcoming reward bucket with the given funds. -pub fn fill_upcoming_reward_bucket(deps: DepsMut, funds: Coin) -> StdResult<()> { - UPCOMING_REWARD_BUCKET.update(deps.storage, |mut upcoming_bucket| -> StdResult<_> { - upcoming_bucket.total = asset::aggregate_coins(&upcoming_bucket.total, &vec![funds])?; - Ok(upcoming_bucket) - })?; - - Ok(()) -} diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index ae9632df..1724734a 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -147,10 +147,8 @@ pub enum ExecuteMsg { }, /// Claims the available rewards Claim, - /// Fills the contract with new rewards. FillRewards, - /// Epoch Changed hook implementation. Creates a new reward bucket for the rewards flowing from /// this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring /// reward bucket (only 21 of them are live at a given moment) @@ -254,19 +252,11 @@ pub struct WithdrawableResponse { pub withdrawable_amount: Uint128, } -/// Response for the Weight query. +/// Response for the Claimable query #[cw_serde] -pub struct BondingWeightResponse { - /// The weight of the address. - pub address: String, - /// The weight of the address at the given timestamp. - pub weight: Uint128, - /// The global weight of the contract. - pub global_weight: Uint128, - /// The share the address has of the rewards at the particular timestamp. - pub share: Decimal, - /// The epoch id at which the weight was calculated. - pub epoch_id: u64, +pub struct ClaimableRewardBucketsResponse { + /// The reward buckets that can be claimed by the address. + pub reward_buckets: Vec, } /// Creates a message to fill rewards on the whale lair contract. @@ -277,9 +267,3 @@ pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult, -} From 55990d1e26488053807c15b0fef210ef59850da3 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 17 May 2024 17:07:12 +0100 Subject: [PATCH 36/51] refactor: cleanup state --- .../bonding-manager/src/bonding/commands.rs | 124 ++++++++++++------ .../bonding-manager/src/contract.rs | 3 +- .../bonding-manager/src/helpers.rs | 17 +-- .../bonding-manager/src/queries.rs | 87 +++++------- .../bonding-manager/src/state.rs | 88 +++++++++++-- .../bonding-manager/src/tests/queries.rs | 19 ++- .../src/tests/unbond_withdraw.rs | 8 +- .../incentive-manager/src/state.rs | 4 +- .../white-whale-std/src/bonding_manager.rs | 11 +- 9 files changed, 239 insertions(+), 122 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index 7f022c52..62bed1e3 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -1,15 +1,17 @@ -use crate::queries::MAX_PAGE_LIMIT; -use crate::state::{ - update_bond_weight, update_global_weight, BOND, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, -}; -use crate::{helpers, ContractError}; use cosmwasm_std::{ - ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Order, Response, - StdResult, Uint128, + ensure, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, + Uint128, }; + use white_whale_std::bonding_manager::Bond; use white_whale_std::pool_network::asset; +use crate::state::{ + get_bonds_by_receiver, update_bond_weight, update_global_weight, BONDS, BOND_COUNTER, CONFIG, + GLOBAL, LAST_CLAIMED_EPOCH, MAX_LIMIT, +}; +use crate::{helpers, ContractError}; + /// Bonds the provided asset. pub(crate) fn bond( mut deps: DepsMut, @@ -28,25 +30,50 @@ pub(crate) fn bond( &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, )?; - let mut bond = BOND - .key((&info.sender, &asset.denom)) - .may_load(deps.storage)? - .unwrap_or(Bond { + let mut bonds_by_receiver = get_bonds_by_receiver( + deps.storage, + info.sender.to_string(), + Some(true), + Some(asset.denom.clone()), + None, + None, + )?; + + let mut bond = if bonds_by_receiver.is_empty() { + // create bond id + let bond_id = + BOND_COUNTER.update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; + + Bond { + id: bond_id, asset: Coin { amount: Uint128::zero(), ..asset.clone() }, created_at_epoch: current_epoch.epoch.id, last_updated: current_epoch.epoch.id, + receiver: info.sender.clone(), ..Bond::default() - }); + } + } else { + ensure!( + bonds_by_receiver.len() == 1usize, + //todo change this error + ContractError::NothingToUnbond + ); + + //todo change this error + bonds_by_receiver + .pop() + .ok_or(ContractError::NothingToUnbond)? + }; // update bond values - bond = update_bond_weight(&mut deps, info.sender.clone(), current_epoch.epoch.id, bond)?; + bond = update_bond_weight(&mut deps, current_epoch.epoch.id, bond)?; bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; bond.weight = bond.weight.checked_add(asset.amount)?; - BOND.save(deps.storage, (&info.sender, &bond.asset.denom), &bond)?; + BONDS.save(deps.storage, bond.id, &bond)?; // update global values let mut global_index = GLOBAL.load(deps.storage)?; @@ -82,10 +109,30 @@ pub(crate) fn unbond( helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps)?; - if let Some(mut unbond) = BOND - .key((&info.sender, &asset.denom)) - .may_load(deps.storage)? - { + + let mut bonds_by_receiver = get_bonds_by_receiver( + deps.storage, + info.sender.to_string(), + Some(true), + Some(asset.denom.clone()), + None, + None, + )?; + + ensure!( + bonds_by_receiver.len() <= 1usize, + //todo change this error + ContractError::NothingToUnbond + ); + + if bonds_by_receiver.is_empty() { + Err(ContractError::NothingToUnbond) + } else { + //todo change this error + let mut unbond: Bond = bonds_by_receiver + .pop() + .ok_or(ContractError::NothingToUnbond)?; + // check if the address has enough bond ensure!( unbond.asset.amount >= asset.amount, @@ -100,32 +147,31 @@ pub(crate) fn unbond( )?; // update bond values, decrease the bond - unbond = update_bond_weight( - &mut deps, - info.sender.clone(), - current_epoch.epoch.id, - unbond.clone(), - )?; + unbond = update_bond_weight(&mut deps, current_epoch.epoch.id, unbond.clone())?; let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); unbond.weight = unbond.weight.saturating_sub(weight_slash); unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); if unbond.asset.amount.is_zero() { - BOND.remove(deps.storage, (&info.sender, &asset.denom)); + BONDS.remove(deps.storage, unbond.id)?; } else { - BOND.save(deps.storage, (&info.sender, &asset.denom), &unbond)?; + BONDS.save(deps.storage, unbond.id, &unbond)?; } + let bond_id = + BOND_COUNTER.update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; // record the unbonding - UNBOND.save( + BONDS.save( deps.storage, - (&info.sender, &asset.denom, env.block.time.seconds()), + bond_id, &Bond { + id: bond_id, asset: asset.clone(), weight: Uint128::zero(), last_updated: current_epoch.epoch.id, created_at_epoch: current_epoch.epoch.id, unbonded_at: Some(env.block.time.seconds()), + receiver: info.sender.clone(), }, )?; // update global values @@ -143,8 +189,6 @@ pub(crate) fn unbond( ("address", info.sender.to_string()), ("asset", asset.to_string()), ])) - } else { - Err(ContractError::NothingToUnbond) } } @@ -154,11 +198,14 @@ pub(crate) fn withdraw( address: Addr, denom: String, ) -> Result { - let unbondings: Vec<(u64, Bond)> = UNBOND - .prefix((&address, &denom)) - .range(deps.storage, None, None, Order::Ascending) - .take(MAX_PAGE_LIMIT as usize) - .collect::>>()?; + let unbondings = get_bonds_by_receiver( + deps.storage, + address.to_string(), + Some(false), + Some(denom.clone()), + None, + Some(MAX_LIMIT), + )?; ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); @@ -170,13 +217,10 @@ pub(crate) fn withdraw( )?; let mut refund_amount = Uint128::zero(); - for unbonding in unbondings { - let (ts, bond) = unbonding; + for bond in unbondings { if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { - let denom = bond.asset.denom; - refund_amount = refund_amount.checked_add(bond.asset.amount)?; - UNBOND.remove(deps.storage, (&address, &denom, ts)); + BONDS.remove(deps.storage, bond.id)?; } } diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index 335ea793..f2b9a379 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -1,6 +1,6 @@ use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; -use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, UPCOMING_REWARD_BUCKET}; +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}; @@ -45,6 +45,7 @@ pub fn instantiate( // Initialize the upcoming reward bucket UPCOMING_REWARD_BUCKET.save(deps.storage, &UpcomingRewardBucket::default())?; + BOND_COUNTER.save(deps.storage, &0)?; Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index d0789534..2b4d444b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -19,8 +19,10 @@ use white_whale_std::pool_network::asset::aggregate_coins; use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; -use crate::queries::{query_claimable, MAX_PAGE_LIMIT}; -use crate::state::{get_weight, BOND, CONFIG, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET}; +use crate::queries::query_claimable; +use crate::state::{ + get_bonds_by_receiver, get_weight, CONFIG, REWARD_BUCKETS, UPCOMING_REWARD_BUCKET, +}; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { @@ -377,19 +379,14 @@ pub(crate) fn get_user_share( address: String, mut global_index: GlobalIndex, ) -> StdResult { - let address = deps.api.addr_validate(&address)?; - - let bonds: StdResult> = BOND - .prefix(&address) - .range(deps.storage, None, None, Order::Ascending) - .take(MAX_PAGE_LIMIT as usize) - .collect(); + let mut bonds_by_receiver = + get_bonds_by_receiver(deps.storage, address, Some(true), None, None, None)?; let config = CONFIG.load(deps.storage)?; let mut total_bond_weight = Uint128::zero(); - for (_, mut bond) in bonds? { + for bond in bonds_by_receiver.iter_mut() { bond.weight = get_weight( epoch_id, bond.weight, diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 2aab70ee..5b490bf2 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -1,15 +1,13 @@ -use cosmwasm_std::{Deps, Order, StdResult, Uint128}; -use cw_storage_plus::Bound; +use cosmwasm_std::{Deps, StdResult, Uint128}; use crate::{helpers, ContractError}; use white_whale_std::bonding_manager::ClaimableRewardBucketsResponse; use white_whale_std::bonding_manager::{ - Bond, BondedResponse, Config, GlobalIndex, RewardsResponse, UnbondingResponse, - WithdrawableResponse, + BondedResponse, Config, GlobalIndex, RewardsResponse, UnbondingResponse, WithdrawableResponse, }; use crate::state::{ - BOND, BONDING_ASSETS_LIMIT, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, REWARD_BUCKETS, UNBOND, + get_bonds_by_receiver, CONFIG, GLOBAL, LAST_CLAIMED_EPOCH, MAX_LIMIT, REWARD_BUCKETS, }; /// Queries the current configuration of the contract. @@ -20,57 +18,46 @@ pub(crate) fn query_config(deps: Deps) -> StdResult { /// Queries the current bonded amount of the given address. If no address is provided, returns /// the global bonded amount. pub(crate) fn query_bonded(deps: Deps, address: Option) -> StdResult { - let (total_bonded, bonded_assets, first_bonded_epoch_id) = if let Some(address) = address { + let (total_bonded, bonded_assets) = if let Some(address) = address { let address = deps.api.addr_validate(&address)?; - let bonds: Vec = BOND - .prefix(&address) - .range(deps.storage, None, None, Order::Ascending) - .take(BONDING_ASSETS_LIMIT) - .map(|item| { - let (_, bond) = item?; - Ok(bond) - }) - .collect::>>()?; + let bonds = get_bonds_by_receiver( + deps.storage, + address.to_string(), + Some(true), + None, + None, + None, + )?; // if it doesn't have bonded, return empty response if bonds.is_empty() { return Ok(BondedResponse { total_bonded: Default::default(), bonded_assets: Default::default(), - first_bonded_epoch_id: Default::default(), }); } let mut total_bonded = Uint128::zero(); let mut bonded_assets = vec![]; - let mut first_bonded_epoch_id = u64::MAX; for bond in bonds { - if bond.created_at_epoch < first_bonded_epoch_id { - first_bonded_epoch_id = bond.created_at_epoch; - } - total_bonded = total_bonded.checked_add(bond.asset.amount)?; bonded_assets.push(bond.asset); } - (total_bonded, bonded_assets, Some(first_bonded_epoch_id)) + (total_bonded, bonded_assets) } else { let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - (global_index.bonded_amount, global_index.bonded_assets, None) + (global_index.bonded_amount, global_index.bonded_assets) }; Ok(BondedResponse { total_bonded, bonded_assets, - first_bonded_epoch_id, }) } -pub const MAX_PAGE_LIMIT: u8 = 30u8; -pub const DEFAULT_PAGE_LIMIT: u8 = 10u8; - /// Queries the current unbonding amount of the given address. pub(crate) fn query_unbonding( deps: Deps, @@ -80,18 +67,16 @@ pub(crate) fn query_unbonding( limit: Option, ) -> StdResult { let address = deps.api.addr_validate(&address)?; - let limit = limit.unwrap_or(DEFAULT_PAGE_LIMIT).min(MAX_PAGE_LIMIT) as usize; - let start = calc_range_start(start_after).map(Bound::ExclusiveRaw); - - let unbonding = UNBOND - .prefix((&deps.api.addr_validate(address.as_str())?, &denom)) - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - let (_, bond) = item?; - Ok(bond) - }) - .collect::>>()?; + + let unbonding = get_bonds_by_receiver( + deps.storage, + address.to_string(), + Some(false), + Some(denom), + start_after, + limit, + )?; + // aggregate all the amounts in unbonding vec and return uint128 let unbonding_amount = unbonding.iter().try_fold(Uint128::zero(), |acc, bond| { acc.checked_add(bond.asset.amount) @@ -103,14 +88,6 @@ pub(crate) fn query_unbonding( }) } -fn calc_range_start(start_after: Option) -> Option> { - start_after.map(|block_height| { - let mut v: Vec = block_height.to_be_bytes().to_vec(); - v.push(0); - v - }) -} - /// Queries the amount of unbonding tokens of the specified address that have passed the /// unbonding period and can be withdrawn. pub(crate) fn query_withdrawable( @@ -118,11 +95,14 @@ pub(crate) fn query_withdrawable( address: String, denom: String, ) -> StdResult { - let unbonding: StdResult> = UNBOND - .prefix((&deps.api.addr_validate(address.as_str())?, &denom)) - .range(deps.storage, None, None, Order::Ascending) - .take(MAX_PAGE_LIMIT as usize) - .collect(); + let unbonding = get_bonds_by_receiver( + deps.storage, + address, + Some(false), + Some(denom), + None, + Some(MAX_LIMIT), + )?; let config = CONFIG.load(deps.storage)?; let current_epoch: white_whale_std::epoch_manager::epoch_manager::EpochResponse = @@ -132,7 +112,7 @@ pub(crate) fn query_withdrawable( )?; let mut withdrawable_amount = Uint128::zero(); - for (_, bond) in unbonding? { + for bond in unbonding { if current_epoch.epoch.id.saturating_sub(bond.created_at_epoch) >= config.unbonding_period { withdrawable_amount = withdrawable_amount.checked_add(bond.asset.amount)?; } @@ -186,6 +166,7 @@ pub fn query_claimable( claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } + println!("here: {:?}", claimable_reward_buckets); Ok(ClaimableRewardBucketsResponse { reward_buckets: claimable_reward_buckets, }) diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index d5102535..e9e16ee4 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,16 +1,41 @@ -use crate::ContractError; -use cosmwasm_std::{Addr, Decimal, DepsMut, StdError, StdResult, Uint128}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{Addr, Decimal, DepsMut, Order, StdError, StdResult, Storage, Uint128}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; + use white_whale_std::bonding_manager::{ Bond, Config, GlobalIndex, RewardBucket, UpcomingRewardBucket, }; -type Denom = str; +use crate::ContractError; pub const BONDING_ASSETS_LIMIT: usize = 2; pub const CONFIG: Item = Item::new("config"); -pub const BOND: Map<(&Addr, &Denom), Bond> = Map::new("bond"); -pub const UNBOND: Map<(&Addr, &Denom, u64), Bond> = Map::new("unbond"); + +/// A monotonically increasing counter to generate unique bond ids. +pub const BOND_COUNTER: Item = Item::new("bond_id_counter"); +pub const BONDS: IndexedMap = IndexedMap::new( + "bonds", + BondIndexes { + receiver: MultiIndex::new(|_pk, b| b.receiver.to_string(), "bonds", "bonds__receiver"), + asset_denom: MultiIndex::new( + |_pk, b| b.asset.denom.to_string(), + "bonds", + "bonds__asset_denom", + ), + }, +); + +pub struct BondIndexes<'a> { + pub receiver: MultiIndex<'a, String, Bond, String>, + pub asset_denom: MultiIndex<'a, String, Bond, String>, +} + +impl<'a> IndexList for BondIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.receiver, &self.asset_denom]; + Box::new(v.into_iter()) + } +} + pub const GLOBAL: Item = Item::new("global"); pub const LAST_CLAIMED_EPOCH: Map<&Addr, u64> = Map::new("last_claimed_epoch"); pub const REWARD_BUCKETS: Map = Map::new("reward_buckets"); @@ -23,7 +48,6 @@ pub const UPCOMING_REWARD_BUCKET: Item = Item::new("upcomi /// Updates the local weight of the given address. pub fn update_bond_weight( deps: &mut DepsMut, - address: Addr, current_epoch_id: u64, mut bond: Bond, ) -> Result { @@ -38,7 +62,7 @@ pub fn update_bond_weight( )?; bond.last_updated = current_epoch_id; - BOND.save(deps.storage, (&address, &bond.asset.denom), &bond)?; + BONDS.save(deps.storage, bond.id, &bond)?; Ok(bond) } @@ -85,3 +109,51 @@ pub fn get_weight( Ok(weight.checked_add(amount.checked_mul(time_factor)? * growth_rate)?) } + +// settings for pagination +pub(crate) const MAX_LIMIT: u8 = 100; +pub const DEFAULT_LIMIT: u8 = 10; + +pub fn get_bonds_by_receiver( + storage: &dyn Storage, + receiver: String, + is_bonding: Option, + asset_denom: Option, + start_after: Option, + limit: Option, +) -> StdResult> { + let start = calc_range_start(start_after).map(Bound::ExclusiveRaw); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + let mut bonds_by_receiver = BONDS + .idx + .receiver + .prefix(receiver) + .range(storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, bond) = item?; + Ok(bond) + }) + .collect::>>()?; + + println!("bonds_by_receiver: {:?}", bonds_by_receiver); + + if let Some(is_bonding) = is_bonding { + bonds_by_receiver.retain(|bond| bond.unbonded_at.is_none() == is_bonding); + } + + if let Some(asset_denom) = asset_denom { + bonds_by_receiver.retain(|bond| bond.asset.denom == asset_denom); + } + + Ok(bonds_by_receiver) +} + +fn calc_range_start(start_after: Option) -> Option> { + start_after.map(|block_height| { + let mut v: Vec = block_height.to_be_bytes().to_vec(); + v.push(0); + v + }) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs b/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs index b399961f..445d0864 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/queries.rs @@ -205,7 +205,6 @@ fn test_queries() { denom: "ampWHALE".to_string(), amount: Uint128::from(1_000u128), }], - first_bonded_epoch_id: None, } ); }); @@ -245,32 +244,40 @@ fn test_queries() { total_amount: Uint128::new(1_000), unbonding_requests: vec![ Bond { + id: 2, asset: coin(100, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572056619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, Bond { + id: 3, asset: coin(200, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572057619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, Bond { + id: 4, asset: coin(300, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572058619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, Bond { + id: 5, asset: coin(400, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572059619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, ], } @@ -290,18 +297,22 @@ fn test_queries() { total_amount: Uint128::new(300), unbonding_requests: vec![ Bond { + id: 2, asset: coin(100, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572056619), last_updated: 3, weight: Default::default(), + receiver: creator.clone(), }, Bond { + id: 3, asset: coin(200, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572057619), last_updated: 3, weight: Default::default(), + receiver: creator.clone(), }, ], } @@ -312,7 +323,7 @@ fn test_queries() { suite.query_unbonding( creator.clone().to_string(), "ampWHALE".to_string(), - Some(1572057619), + Some(3), Some(2), |res| { let unbonding_response = res.unwrap().1; @@ -322,18 +333,22 @@ fn test_queries() { total_amount: Uint128::new(700), unbonding_requests: vec![ Bond { + id: 4, asset: coin(300, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572058619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, Bond { + id: 5, asset: coin(400, "ampWHALE"), created_at_epoch: 3, unbonded_at: Some(1572059619), last_updated: 3, weight: Default::default(), + receiver: creator.clone() }, ], } 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 47ede32b..4bf30267 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/unbond_withdraw.rs @@ -502,11 +502,13 @@ fn test_unbonding_withdraw() { assert_eq!( unbonding_response.unbonding_requests[0], Bond { + id: 4, asset: coin(1_000u128, "ampWHALE"), created_at_epoch: 6, unbonded_at: Some(1572335819), last_updated: 6, weight: Uint128::zero(), + receiver: creator.clone(), } ); }, @@ -724,7 +726,6 @@ fn test_unbonding_withdraw() { BondedResponse { total_bonded: Uint128::new(200), bonded_assets: coins(200u128, "bWHALE"), - first_bonded_epoch_id: Some(4u64), } ); }) @@ -741,18 +742,22 @@ fn test_unbonding_withdraw() { total_amount: Uint128::new(500), unbonding_requests: vec![ Bond { + id: 5, asset: coin(300u128, "bWHALE"), created_at_epoch: 8, unbonded_at: Some(1572508619), last_updated: 8, weight: Uint128::zero(), + receiver: another_sender.clone(), }, Bond { + id: 6, asset: coin(200u128, "bWHALE"), created_at_epoch: 8, unbonded_at: Some(1572508620), last_updated: 8, weight: Uint128::zero(), + receiver: another_sender.clone(), } ], } @@ -964,7 +969,6 @@ fn test_unbonding_withdraw() { BondedResponse { total_bonded: Uint128::new(200), bonded_assets: coins(200u128, "bWHALE"), - first_bonded_epoch_id: Some(4u64), } ); }) diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index d6430e94..245d539f 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -11,7 +11,7 @@ use crate::ContractError; /// Contract's config pub const CONFIG: Item = Item::new("config"); -/// An monotonically increasing counter to generate unique position identifiers. +/// A monotonically increasing counter to generate unique position identifiers. pub const POSITION_ID_COUNTER: Item = Item::new("position_id_counter"); /// The positions that a user has. Positions can be open or closed. @@ -47,7 +47,7 @@ pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch pub const LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = Map::new("address_lp_weight_history"); -/// An monotonically increasing counter to generate unique incentive identifiers. +/// A monotonically increasing counter to generate unique incentive identifiers. pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); /// Incentives map diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 1724734a..0259919a 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -54,6 +54,8 @@ pub struct UpcomingRewardBucket { #[cw_serde] pub struct Bond { + /// The id of the bond. + pub id: u64, /// 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. @@ -64,11 +66,14 @@ pub struct Bond { pub weight: Uint128, /// The time at which the Bond was unbonded. pub unbonded_at: Option, + /// The owner of the bond. + pub receiver: Addr, } impl Default for Bond { fn default() -> Self { Self { + id: 0, asset: Coin { denom: String::new(), amount: Uint128::zero(), @@ -77,6 +82,7 @@ impl Default for Bond { unbonded_at: None, last_updated: Default::default(), weight: Uint128::zero(), + receiver: Addr::unchecked(""), } } } @@ -229,11 +235,8 @@ pub struct BondedResponse { /// The total amount of bonded tokens by the address. Bear in mind the bonded assets are /// considered to be equal for this purpose. pub total_bonded: Uint128, - /// The total amount of bonded assets by the address. + /// The assets that are bonded by the address. pub bonded_assets: Vec, - /// If Some, the epoch id at which the user/address bonded first time. None is used when this - /// Response is used to check the bonded assets in the contract. - pub first_bonded_epoch_id: Option, } /// Response for the Unbonding query From 01cf9730ccdc003b210caaa67f5f04b3f72467cb Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 17 May 2024 17:29:36 +0100 Subject: [PATCH 37/51] chore: final refactor --- .../schema/bonding-manager.json | 32 +++++++++++------ .../schema/raw/response_to_bonded.json | 11 +----- .../schema/raw/response_to_claimable.json | 1 + .../schema/raw/response_to_unbonding.json | 20 +++++++++++ .../bonding-manager/src/bonding/commands.rs | 30 ++++++++-------- .../bonding-manager/src/contract.rs | 2 +- .../bonding-manager/src/queries.rs | 1 - .../bonding-manager/src/state.rs | 10 +----- .../bonding-manager/src/tests/bond.rs | 34 ++++++++++++++++++- 9 files changed, 94 insertions(+), 47 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 8fe857a7..7951283d 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -592,21 +592,12 @@ ], "properties": { "bonded_assets": { - "description": "The total amount of bonded assets by the address.", + "description": "The assets that are bonded by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, - "first_bonded_epoch_id": { - "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, "total_bonded": { "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", "allOf": [ @@ -642,6 +633,7 @@ "claimable": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimableRewardBucketsResponse", + "description": "Response for the Claimable query", "type": "object", "required": [ "reward_buckets" @@ -1089,12 +1081,18 @@ }, "additionalProperties": false, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "Bond": { "type": "object", "required": [ "asset", "created_at_epoch", + "id", "last_updated", + "receiver", "weight" ], "properties": { @@ -1112,12 +1110,26 @@ "format": "uint64", "minimum": 0.0 }, + "id": { + "description": "The id of the bond.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "last_updated": { "description": "The epoch id at which the bond was last time updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, + "receiver": { + "description": "The owner of the bond.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "unbonded_at": { "description": "The time at which the Bond was unbonded.", "type": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json index 6deb74cb..233b61a6 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json @@ -9,21 +9,12 @@ ], "properties": { "bonded_assets": { - "description": "The total amount of bonded assets by the address.", + "description": "The assets that are bonded by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, - "first_bonded_epoch_id": { - "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, "total_bonded": { "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", "allOf": [ 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 788b95ed..c46decf7 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 @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimableRewardBucketsResponse", + "description": "Response for the Claimable query", "type": "object", "required": [ "reward_buckets" 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 5181a615..a72e62fb 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 @@ -26,12 +26,18 @@ }, "additionalProperties": false, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "Bond": { "type": "object", "required": [ "asset", "created_at_epoch", + "id", "last_updated", + "receiver", "weight" ], "properties": { @@ -49,12 +55,26 @@ "format": "uint64", "minimum": 0.0 }, + "id": { + "description": "The id of the bond.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "last_updated": { "description": "The epoch id at which the bond was last time updated.", "type": "integer", "format": "uint64", "minimum": 0.0 }, + "receiver": { + "description": "The owner of the bond.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "unbonded_at": { "description": "The time at which the Bond was unbonded.", "type": [ diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index 62bed1e3..c93c7e1a 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -30,7 +30,7 @@ pub(crate) fn bond( &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, )?; - let mut bonds_by_receiver = get_bonds_by_receiver( + let bonds_by_receiver = get_bonds_by_receiver( deps.storage, info.sender.to_string(), Some(true), @@ -40,7 +40,8 @@ pub(crate) fn bond( )?; let mut bond = if bonds_by_receiver.is_empty() { - // create bond id + // the user doesn't have any bonds of the given asset + let bond_id = BOND_COUNTER.update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; @@ -56,16 +57,13 @@ pub(crate) fn bond( ..Bond::default() } } else { + // sanity check ensure!( bonds_by_receiver.len() == 1usize, - //todo change this error - ContractError::NothingToUnbond + ContractError::AssetMismatch ); - //todo change this error - bonds_by_receiver - .pop() - .ok_or(ContractError::NothingToUnbond)? + bonds_by_receiver[0].clone() }; // update bond values @@ -110,7 +108,7 @@ pub(crate) fn unbond( helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps)?; - let mut bonds_by_receiver = get_bonds_by_receiver( + let bonds_by_receiver = get_bonds_by_receiver( deps.storage, info.sender.to_string(), Some(true), @@ -121,17 +119,19 @@ pub(crate) fn unbond( ensure!( bonds_by_receiver.len() <= 1usize, - //todo change this error - ContractError::NothingToUnbond + ContractError::AssetMismatch ); if bonds_by_receiver.is_empty() { Err(ContractError::NothingToUnbond) } else { - //todo change this error - let mut unbond: Bond = bonds_by_receiver - .pop() - .ok_or(ContractError::NothingToUnbond)?; + // sanity check + ensure!( + bonds_by_receiver.len() == 1usize, + ContractError::AssetMismatch + ); + + let mut unbond = bonds_by_receiver[0].clone(); // check if the address has enough bond ensure!( diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index f2b9a379..c282a13c 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -43,7 +43,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - // Initialize the upcoming reward bucket + // Initialize the upcoming reward bucket and bond counter UPCOMING_REWARD_BUCKET.save(deps.storage, &UpcomingRewardBucket::default())?; BOND_COUNTER.save(deps.storage, &0)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 5b490bf2..c8926db0 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -166,7 +166,6 @@ pub fn query_claimable( claimable_reward_buckets.retain(|bucket| !bucket.available.is_empty()); } - println!("here: {:?}", claimable_reward_buckets); Ok(ClaimableRewardBucketsResponse { reward_buckets: claimable_reward_buckets, }) diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index e9e16ee4..738de7c5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -16,22 +16,16 @@ pub const BONDS: IndexedMap = IndexedMap::new( "bonds", BondIndexes { receiver: MultiIndex::new(|_pk, b| b.receiver.to_string(), "bonds", "bonds__receiver"), - asset_denom: MultiIndex::new( - |_pk, b| b.asset.denom.to_string(), - "bonds", - "bonds__asset_denom", - ), }, ); pub struct BondIndexes<'a> { pub receiver: MultiIndex<'a, String, Bond, String>, - pub asset_denom: MultiIndex<'a, String, Bond, String>, } impl<'a> IndexList for BondIndexes<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.receiver, &self.asset_denom]; + let v: Vec<&dyn Index> = vec![&self.receiver]; Box::new(v.into_iter()) } } @@ -137,8 +131,6 @@ pub fn get_bonds_by_receiver( }) .collect::>>()?; - println!("bonds_by_receiver: {:?}", bonds_by_receiver); - if let Some(is_bonding) = is_bonding { bonds_by_receiver.retain(|bond| bond.unbonded_at.is_none() == is_bonding); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs index 7d882e5c..11e825dd 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/bond.rs @@ -1,6 +1,7 @@ +use cosmwasm_std::{coin, coins}; + use crate::tests::suite::TestingSuite; use crate::ContractError; -use cosmwasm_std::{coin, coins}; #[test] fn test_bond_unsuccessful() { @@ -48,3 +49,34 @@ fn test_bond_unsuccessful() { }, ); } + +#[test] +fn test_same_bond_multiple_times() { + let mut suite = TestingSuite::default(); + let creator = suite.senders[0].clone(); + + suite + .instantiate_default() + .add_one_day() + .create_new_epoch() + .bond( + creator.clone(), + &vec![coin(1_000u128, "bWHALE")], + |result| { + result.unwrap(); + }, + ) + .bond( + creator.clone(), + &vec![coin(2_000u128, "bWHALE")], + |result| { + result.unwrap(); + }, + ) + .query_bonded(Some(creator.clone().to_string()), |res| { + assert_eq!( + res.unwrap().1.bonded_assets, + vec![coin(3_000u128, "bWHALE")] + ); + }); +} From 64176dddfd08071fb05065258ce35226cb4fdca9 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 20 May 2024 11:49:05 +0100 Subject: [PATCH 38/51] test: use test_case for extract_pool_identifier unit test --- Cargo.lock | 1 + .../liquidity_hub/bonding-manager/Cargo.toml | 1 + .../bonding-manager/src/tests/helpers.rs | 64 +++++++++---------- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b11f13a4..f89e05de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,7 @@ dependencies = [ "schemars", "semver", "serde", + "test-case", "thiserror", "white-whale-std", "white-whale-testing", diff --git a/contracts/liquidity_hub/bonding-manager/Cargo.toml b/contracts/liquidity_hub/bonding-manager/Cargo.toml index a00aaa01..3a12f0f3 100644 --- a/contracts/liquidity_hub/bonding-manager/Cargo.toml +++ b/contracts/liquidity_hub/bonding-manager/Cargo.toml @@ -48,3 +48,4 @@ anyhow.workspace = true white-whale-testing.workspace = true pool-manager.workspace = true epoch-manager.workspace = true +test-case.workspace = true diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs index 44b0fab3..2979b2a8 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/helpers.rs @@ -1,36 +1,32 @@ use crate::helpers::extract_pool_identifier; - -#[test] -fn test_extract_pool_identifier() { - let denom_1 = "non_whitelisted_asset"; - let denom_2 = "factory/contract100/uluna-uwhale.pool.random_identifier.uLP"; - let denom_3 = "factory/contract100/uluna-uwhale.pool..pool./.pool.crazy.pool.identifier.uLP"; - let denom_4 = "factory/contract100/uluna-uwhale.pool.messy_.pool._identifier.uLP"; - let denom_5 = "factory/contract100/uluna-uwhale.pool./hacky_.pool./_identifier.uLP"; - let denom_6 = "factory/contract100/uluna-uwhale.pair.1.uLP"; - let denom_7 = "factory/contract100/uluna-uwhale.pair.1"; - let denom_8 = "factory/contract100/bWHALE"; - - let res = extract_pool_identifier(denom_1); - assert!(res.is_none()); - - let res = extract_pool_identifier(denom_2); - assert_eq!(res.unwrap(), "random_identifier"); - let res = extract_pool_identifier(denom_3); - assert_eq!(res.unwrap(), ".pool./.pool.crazy.pool.identifier"); - - let res = extract_pool_identifier(denom_4); - assert_eq!(res.unwrap(), "messy_.pool._identifier"); - - let res = extract_pool_identifier(denom_5); - assert_eq!(res.unwrap(), "/hacky_.pool./_identifier"); - - let res = extract_pool_identifier(denom_6); - assert!(res.is_none()); - - let res = extract_pool_identifier(denom_7); - assert!(res.is_none()); - - let res = extract_pool_identifier(denom_8); - assert!(res.is_none()); +use test_case::test_case; + +#[test_case("non_whitelisted_asset", None)] +#[test_case( + "factory/contract1/uluna-uwhale.pool.random_identifier.uLP", + Some("random_identifier") +)] +#[test_case( + "factory/contract2/uluna-uwhale.pool..pool./.pool.crazy.pool.identifier.uLP", + Some(".pool./.pool.crazy.pool.identifier") +)] +#[test_case( + "factory/contract3/uluna-uwhale.pool.messy_.pool._identifier.uLP", + Some("messy_.pool._identifier") +)] +#[test_case( + "factory/contract4/uluna-uwhale.pool./hacky_.pool./_identifier.uLP", + Some("/hacky_.pool./_identifier") +)] +#[test_case("factory/contract5/uluna-uwhale.pair.1.uLP", None)] +#[test_case("factory/contract6/uluna-uwhale.pair.1", None)] +#[test_case("factory/contract7/bWHALE", None)] +fn test_extract_pool_identifier(denom: &str, expected: Option<&str>) { + let res = extract_pool_identifier(denom); + + if res.is_none() { + assert!(expected.is_none()); + } else { + assert_eq!(res.unwrap(), expected.unwrap()); + } } From 35aaa12158ff124c8f971288eeb893381286c2e8 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 20 May 2024 15:14:47 +0100 Subject: [PATCH 39/51] docs: add readme to bonding-manager --- .github/ISSUE_TEMPLATE/1_bug.md | 15 ++-- .github/ISSUE_TEMPLATE/2_improvement.md | 18 +++-- .github/ISSUE_TEMPLATE/3_feature_request.md | 9 +-- .github/ISSUE_TEMPLATE/4_infrastructure.md | 7 +- .github/ISSUE_TEMPLATE/5_protocol.md | 9 +-- README.md | 73 ++++++++++--------- contracts/liquidity_hub/README.md | 25 ++++--- .../liquidity_hub/bonding-manager/README.md | 49 ++++++++++++- .../liquidity_hub/incentive-manager/README.md | 2 +- .../liquidity_hub/pool-manager/README.md | 28 ++++--- .../liquidity_hub/pool-manager/ROUTER.md | 8 +- .../liquidity_hub/pool-network/README.md | 4 +- .../pool-network/terraswap_token/README.md | 2 +- .../liquidity_hub/vault-manager/README.md | 2 +- docs/CODE_OF_CONDUCT.md | 3 + docs/SECURITY.md | 18 +++-- justfile | 1 + packages/white-whale-std/README.md | 2 +- scripts/README.md | 2 +- scripts/utils/format_md.sh | 15 ++++ 20 files changed, 188 insertions(+), 104 deletions(-) create mode 100755 scripts/utils/format_md.sh diff --git a/.github/ISSUE_TEMPLATE/1_bug.md b/.github/ISSUE_TEMPLATE/1_bug.md index b8d2d8f3..6332f31d 100644 --- a/.github/ISSUE_TEMPLATE/1_bug.md +++ b/.github/ISSUE_TEMPLATE/1_bug.md @@ -1,10 +1,9 @@ --- name: Bug on migaloo-core contracts about: You are interacting with White Whale's contracts, and you are getting an unexpected behavior, an exception, or something looks wrong. -title: '' -labels: 'bug' -assignees: '' - +title: "" +labels: "bug" +assignees: "" --- @@ -37,17 +37,19 @@ If you want to report a security issue, please follow our security policy: `http --- ### Additional context + Add any other context about the problem here, code snippets, json responses and so on.
Code sample ```rust + ```
@@ -56,7 +58,7 @@ Add any other context about the problem here, code snippets, json responses and Logs ``` + ``` diff --git a/.github/ISSUE_TEMPLATE/2_improvement.md b/.github/ISSUE_TEMPLATE/2_improvement.md index e99f16c6..be5ab93f 100644 --- a/.github/ISSUE_TEMPLATE/2_improvement.md +++ b/.github/ISSUE_TEMPLATE/2_improvement.md @@ -1,10 +1,9 @@ --- name: Improvement proposal about: General improvements to the protocol. Anything that exists on Migaloo, is working well but could be enhanced or optimized. -title: '' -labels: 'enhancement' -assignees: '' - +title: "" +labels: "enhancement" +assignees: "" --- ```rust + ``` @@ -51,7 +53,7 @@ Add any other context here, code snippets, json responses, screenshots and so on Logs ``` + ``` @@ -75,10 +78,11 @@ Add any other context here, code snippets, json responses, screenshots and so on --> ``` + ``` \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/3_feature_request.md b/.github/ISSUE_TEMPLATE/3_feature_request.md index efe77d88..15b6a6d8 100644 --- a/.github/ISSUE_TEMPLATE/3_feature_request.md +++ b/.github/ISSUE_TEMPLATE/3_feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest a new idea for White Whale Migaloo. This is something that doesn't exist currently, and you want to see. -title: '' -labels: 'enhancement' -assignees: '' - +title: "" +labels: "enhancement" +assignees: "" --- \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/4_infrastructure.md b/.github/ISSUE_TEMPLATE/4_infrastructure.md index 8d1d355b..f0dd0138 100644 --- a/.github/ISSUE_TEMPLATE/4_infrastructure.md +++ b/.github/ISSUE_TEMPLATE/4_infrastructure.md @@ -1,10 +1,9 @@ --- name: The CI infrastructure used by Migaloo has a problem about: As a contributor, you want to file an issue about the build/test infra, e.g. Github actions etc. -title: '' -labels: 'infra' -assignees: '' - +title: "" +labels: "infra" +assignees: "" --- E click B "https://github.com/White-Whale-Defi-Platform/white-whale-core/tree/release/v2_contracts/contracts/liquidity_hub/bonding-manager" "Bonding Manager" click E "https://github.com/White-Whale-Defi-Platform/white-whale-core/tree/release/v2_contracts/contracts/liquidity_hub/epoch-manager" "Epoch Manager" ``` + The direction of the arrows represents the dependencies between the contracts. ### Pool Manager -The Pool Manager is the contract that manages the pools in the protocol. It is responsible for creating pool and handling -swaps. Pool creation is permisionless, meaning anyone can create a pool if the fee is paid. The Pool Manager depends on + +The Pool Manager is the contract that manages the pools in the protocol. It is responsible for creating pool and handling +swaps. Pool creation is permisionless, meaning anyone can create a pool if the fee is paid. The Pool Manager depends on both the Incentive and Bonding Managers. ### Vault Manager -The Vault Manager is the contract that manages the vaults in the protocol. It is responsible for creating vaults and performing -flashloan operations. Vault creation is permissionless, meaning anyone can create a vault if the fee is paid. The Vault Manager + +The Vault Manager is the contract that manages the vaults in the protocol. It is responsible for creating vaults and performing +flashloan operations. Vault creation is permissionless, meaning anyone can create a vault if the fee is paid. The Vault Manager depends on the Bonding Manager, as that's where the flashloan fees are sent for distribution. ### Incentive Manager -The Incentive Manager is the contract that manages the incentives in the protocol. It is responsible for creating and -distributing incentives on pools. Incentive creation is permissionless, meaning anyone can create an incentive if the fee is paid. + +The Incentive Manager is the contract that manages the incentives in the protocol. It is responsible for creating and +distributing incentives on pools. Incentive creation is permissionless, meaning anyone can create an incentive if the fee is paid. The Incentive Manager depends on the Epoch Manager, as incentives are distributed based on epochs. ### Bonding Manager -The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens -and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends + +The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens +and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends on the Epoch Manager, as the fee distribution is done based on epochs. ### Epoch Manager -The Epoch Manager is the contract that manages the epochs in the protocol. Its single responsibility is to create the epochs, + +The Epoch Manager is the contract that manages the epochs in the protocol. Its single responsibility is to create the epochs, which are used by the Incentive and Bonding Managers for distributing incentives and fees. ---- +--- + ## Deployed contracts -White Whale is a protocol that exists across multiple chains. You can find contract addresses for different chain deployments +White Whale is a protocol that exists across multiple chains. You can find contract addresses for different chain deployments in the [documentation](https://docs.whitewhale.money/white-whale/smart-contracts/liquidity-hub-deployments). ## Building and Deploying White Whale -To build and deploy White Whale's smart contracts, there are a series of deployment scripts under `scripts/`. Alternatively, -there are a few `just` recipes you can take advantage of. You need at least Rust v1.65.0 to compile the contracts. +To build and deploy White Whale's smart contracts, there are a series of deployment scripts under `scripts/`. Alternatively, +there are a few `just` recipes you can take advantage of. You need at least Rust v1.65.0 to compile the contracts. ### Build scripts - `build_release.sh`: builds the project artifacts, optimized for production. - `build_schemas.sh`: generates schemas for the contracts. -- `check_artifacts_size.sh`: validates the size of the optimized artifacts. The default maximum size is 600 kB, though -it is customizable by passing the number of kB to the script. For example `check_artifacts_size.sh 400` verifies the -artifacts are under 400 kB. +- `check_artifacts_size.sh`: validates the size of the optimized artifacts. The default maximum size is 600 kB, though + it is customizable by passing the number of kB to the script. For example `check_artifacts_size.sh 400` verifies the + artifacts are under 400 kB. ### Just recipes All recipes are found in the `justfile`. To see all available recipes, run `just` or `just --list`. Here are some of them: -- `build FEATURE=''` # Builds the whole project with the a feature flag if provided. -- `fmt` # Formats the rust, toml and sh files in the project. -- `get-pools CHAIN` # Extracts the pools from the given chain. -- `schemas` # Generates the schemas for the contracts. +- `build FEATURE=''` # Builds the whole project with the a feature flag if provided. +- `fmt` # Formats the rust, toml and sh files in the project. +- `get-pools CHAIN` # Extracts the pools from the given chain. +- `schemas` # Generates the schemas for the contracts. ### Deployment scripts @@ -119,7 +126,7 @@ TODO update deployment scripts for V2. ## Testing White Whale -To run the tests, run `cargo test`. You can also run `cargo tarpaulin -v` to get test code coverage. Note that the White Whale +To run the tests, run `cargo test`. You can also run `cargo tarpaulin -v` to get test code coverage. Note that the White Whale project contains a few feature flags that can be used to run the tests. If you want to run the tests for a particular feature, run `cargo test --features "feature_name"`. diff --git a/contracts/liquidity_hub/README.md b/contracts/liquidity_hub/README.md index d38be40a..556f3e41 100644 --- a/contracts/liquidity_hub/README.md +++ b/contracts/liquidity_hub/README.md @@ -1,16 +1,17 @@ # Liquidity Hub Liquidity Hubs (LH) are established on Cosmos blockchains selected by WW governance. The Liquidity Hubs are the point of -interaction for bots and depositors with the WW protocol and infrastructure. Arb bots request flash loans to arb price +interaction for bots and depositors with the WW protocol and infrastructure. Arb bots request flash loans to arb price differences between WW LPs and Local Dex LPs. Depositors earn fees for their deposits into vaults used for flash loans or into LPs accessible by bots. The LH components are described below. ### Pool network -The pool network is a WW-controlled collection of Automated Market Maker (AMM) pools of WW-selected tokens (WW pools) + +The pool network is a WW-controlled collection of Automated Market Maker (AMM) pools of WW-selected tokens (WW pools) relevant to the chain of the pool network (e.g., Atom-Luna and Atom-Juno LPs on both Terra and Juno chains). The token price -exchange in each pool in the local pool network loosely represents the “interchain price” for the token because when the +exchange in each pool in the local pool network loosely represents the “interchain price” for the token because when the price changes of one pool on one network (e.g., Juno) then the Interchain Command will take action to balance the price of the -same pool on the other network(s) (e.g., Terra). +same pool on the other network(s) (e.g., Terra). These WW pools allow bots to arb the local dexes versus the “interchain price” that effectively decreases price disparities. @@ -21,8 +22,9 @@ The Pool Network is comprised by the following components: - WW Pool: pools holding token pairs. ### Vault Network -The Vault Network is a WW-controlled collection of single asset vaults that bots use to access flashloan capital for arbitrage, -liquidation, and other DeFi uses. By using the Flash Loan Vaults, arbitrage and liquidations happen locally in a capital-efficient + +The Vault Network is a WW-controlled collection of single asset vaults that bots use to access flashloan capital for arbitrage, +liquidation, and other DeFi uses. By using the Flash Loan Vaults, arbitrage and liquidations happen locally in a capital-efficient manner and without capital requirements. The Vault Network is comprised by the following components: @@ -32,9 +34,10 @@ The Vault Network is comprised by the following components: - Vault Router: Routes request to flash-loan assets to the specific vaults ### Fee collector -The local fee collector contract charges the fees for usage of the pool and vault network on a specific blockchain. -Part of the collected fees goes back to the depositors of the WW pools and/or the single asset flash loan vaults. -The remainder is sent to the interchain collector as protocol revenue. The protocol revenue is then distributed to WHALE + +The local fee collector contract charges the fees for usage of the pool and vault network on a specific blockchain. +Part of the collected fees goes back to the depositors of the WW pools and/or the single asset flash loan vaults. +The remainder is sent to the interchain collector as protocol revenue. The protocol revenue is then distributed to WHALE stakers in the form of token buybacks. ### Graphic Overview @@ -48,7 +51,7 @@ flowchart LR PF --> PB[(Pool B)] PF --> PC[(Pool C)] end - + subgraph VN [Vault Network] VF[Vault Factory] --> VA((Vault A)) VF --> VB((Vault B)) @@ -60,7 +63,7 @@ flowchart LR PC --> FC VA --> FC VB --> FC - end + end style LH fill:#0000,stroke:#3CCD64 style PN fill:#0000 diff --git a/contracts/liquidity_hub/bonding-manager/README.md b/contracts/liquidity_hub/bonding-manager/README.md index 2bff19ce..f4fc2c27 100644 --- a/contracts/liquidity_hub/bonding-manager/README.md +++ b/contracts/liquidity_hub/bonding-manager/README.md @@ -1,3 +1,50 @@ # Bonding Manager -The Bonding Manager is the evolution of the Whale Lair, fee distributor and fee collector. It is a bonding contract used to bond WHALE LSDs, collect fees from pools and distribute them a rewards to bonders \ No newline at end of file +The Bonding Manager is the evolution of the Whale Lair, fee distributor and fee collector. It is a bonding contract used +to bond WHALE LSDs, collect fees from pools/vaults and distribute them as rewards to bonders. + +## How it works + +The following is a high-level overview of how the Bonding Manager works. It touches on some technical details, assisting +developers in understanding the contract's inner workings, while also providing a general understanding of the contract's +functionality so a regular user can understand how to interact with it. + +### Rewards + +Users must bond WHALE LSDs to the Bonding Manager to start earning rewards. Every time there's a swap on the pool manager +or a flashloan taken on the vault manager, a fee is collected and sent to the Bonding Manager via the `FillRewards` message. +The rewards collected within an epoch are temporarily stored in the `UPCOMING_REWARD_BUCKET`, which is then distributed to bonders +when the next epoch is created via the Epoch Manager contract, which has a hook to the Bonding Manager calling `EpochChangedHook`. + +When `EpochChangedHook` is called, a new reward bucket is created from the rewards collected in the previous epoch +(stored in `UPCOMING_REWARD_BUCKET`), with a snapshot of the global index stored in `GLOBAL`. The `GlobalIndex` struct +contains information about the total bonded WHALE LSDs, the epoch id it of the snapshot, when (epoch id) was it last updated, +and most importantly, the global weight. The weight is a formula based on how many assets are in the contract and a time factor, +that rewards users that have bonded the longer. The weight is used to calculate the rewards for each bonder when the +claim takes place. + +### Bonding and Withdrawing + +The bonding process is simple. Users call the `Bond` message passing the amount of WHALE LSD they want to bond. This is +stored as a `Bond` struct within the `BONDS` indexed map. The `Bond`struct contains information about the amount of LSDs +the user has bonded, the epoch at which it was created, when (epoch id) was it last updated, and the weight of the bond. + +There's an unbonding period set up when the contract is instantiated. Whenever a user wants to withdraw their funds, it +needs to call the `Unbond` message specifying the amount of tokens it wants to unbond. This action will create a `Bond` +with an `unbonded_at` field, which is the timestamp when the user will be able to withdraw the funds. That value is calculated +based on the unbonding period. Once the `unbonded_at` timestamp is reached, the user can call the `Withdraw` to get the +funds back. + +### Claiming + +Rewards, stored in reward buckets, are only distributed to users that have bonded assets. Users can only claim rewards +from the next epoch after the one they have bonded (no retroactive rewards). The rewards are calculated based on the +user's weight and the global weight for a given epoch. + +To claim, users need to call the `Claim` message, which will return all the rewards they are entitled to from the claimable +reward buckets. The rewards buckets expire after a number of epochs set up when the contract is instantiated. Once a reward +bucket expires, it gets forwarded to a newly created reward bucket when a new epoch is created, i.e. when `EpochChangedHook` +is called by the Epoch Manager. + +Once a user claims, the epoch id at which the user claimed is stored in `LAST_CLAIMED_EPOCH`. This helps computing the +rewards buckets a user can claim from. diff --git a/contracts/liquidity_hub/incentive-manager/README.md b/contracts/liquidity_hub/incentive-manager/README.md index 6cada5e4..fda84ecb 100644 --- a/contracts/liquidity_hub/incentive-manager/README.md +++ b/contracts/liquidity_hub/incentive-manager/README.md @@ -1,4 +1,4 @@ # Incentive Manager The Incentive Manager is the V2 iteration of the original incentives. This is a monolithic contract that handles all -the incentives-related logic. \ No newline at end of file +the incentives-related logic. diff --git a/contracts/liquidity_hub/pool-manager/README.md b/contracts/liquidity_hub/pool-manager/README.md index 02ccc20f..ff8bb7cc 100644 --- a/contracts/liquidity_hub/pool-manager/README.md +++ b/contracts/liquidity_hub/pool-manager/README.md @@ -7,13 +7,12 @@ To understand the framework better, please read the overview in the and dig into the [cosmwasm docs](https://www.cosmwasm.com). This assumes you understand the theory and just want to get coding. - -# Architecture +# Architecture ```mermaid -graph LR +graph LR -subgraph WHALE Dex +subgraph WHALE Dex subgraph Pool Manager Admin --> |Can Create| Pairs Users --> |Can Create for X Fee| Pairs --> | Stored In| Contract @@ -22,7 +21,7 @@ subgraph Pool Manager Users --> |Can Withdraw Liquidity from | Contract --> |Burn provided | Liquidity_Token --> |Send refund assets to| Users - + end end @@ -31,16 +30,15 @@ end ### Todo work -+ [X] Create a pool manager contract -+ [X] Can create a pair -+ [X] Can provide liquidity -+ [X] Can withdraw liquidity -+ [X] Can swap assets -+ [ ] Tests for all the above ported over -+ [ ] Tests for Swap Operations -+ [ ] Tests for swap routes -+ [ ] - +- [x] Create a pool manager contract +- [x] Can create a pair +- [x] Can provide liquidity +- [x] Can withdraw liquidity +- [x] Can swap assets +- [ ] Tests for all the above ported over +- [ ] Tests for Swap Operations +- [ ] Tests for swap routes +- [ ] ## Creating a new repo from template diff --git a/contracts/liquidity_hub/pool-manager/ROUTER.md b/contracts/liquidity_hub/pool-manager/ROUTER.md index 6983da6f..f816ee67 100644 --- a/contracts/liquidity_hub/pool-manager/ROUTER.md +++ b/contracts/liquidity_hub/pool-manager/ROUTER.md @@ -1,10 +1,10 @@ -# Pool Manager router +# Pool Manager router Previously in the pool-network repo a router contract is used to perform multihop swaps which involve more than 1 hop or route to complete a swap. -This functionality is needed in the pool manager but the implementation needs to be different. +This functionality is needed in the pool manager but the implementation needs to be different. In the pool manager there are many pairs and its possible to have multiple pairs with the same assets. For this reason we can't deterministically evaluate a pair key from just asstes. Instead a pool_idenitifer is used to identify a pair and each pair has one. This is what is used to specify a pair in the pool manager. -A multihop swap with this pair manager contract could look like the below +A multihop swap with this pair manager contract could look like the below ``` routes: @@ -16,4 +16,4 @@ routes: ``` The above route would swap from a given route to uosmo and then from uosmo to uion. -Becuase the route is specified by pool_id we can have multiple routes with the same assets but when providing a pool ID and the asset we want we can determine the asset its trading against. \ No newline at end of file +Becuase the route is specified by pool_id we can have multiple routes with the same assets but when providing a pool ID and the asset we want we can determine the asset its trading against. diff --git a/contracts/liquidity_hub/pool-network/README.md b/contracts/liquidity_hub/pool-network/README.md index 165bf973..c64763cb 100644 --- a/contracts/liquidity_hub/pool-network/README.md +++ b/contracts/liquidity_hub/pool-network/README.md @@ -4,8 +4,8 @@ White Whale's pool network is based on [Terraswap](https://github.com/terraswap/ ## Contracts -| Name | Description | -| -------------------------------------------------- | -------------------------------------------- | +| Name | Description | +| ---------------------------------------- | -------------------------------------------- | | [`terraswap_factory`](terraswap_factory) | | | [`terraswap_pair`](terraswap_pair) | | | [`terraswap_router`](terraswap_router) | | diff --git a/contracts/liquidity_hub/pool-network/terraswap_token/README.md b/contracts/liquidity_hub/pool-network/terraswap_token/README.md index 5324c960..55221ac6 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_token/README.md +++ b/contracts/liquidity_hub/pool-network/terraswap_token/README.md @@ -8,4 +8,4 @@ Implements: - [x] CW20 Base - [ ] Mintable extension -- [ ] Allowances extension \ No newline at end of file +- [ ] Allowances extension diff --git a/contracts/liquidity_hub/vault-manager/README.md b/contracts/liquidity_hub/vault-manager/README.md index 6460cda1..ee739335 100644 --- a/contracts/liquidity_hub/vault-manager/README.md +++ b/contracts/liquidity_hub/vault-manager/README.md @@ -1,4 +1,4 @@ # Vault Manager The Vault Manager is the V2 iteration of the original WW vault network. This is a monolithic contract that handles all -the vaults and flashloans in the White Whale DEX. \ No newline at end of file +the vaults and flashloans in the White Whale DEX. diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md index cc6e0472..4b48f65d 100644 --- a/docs/CODE_OF_CONDUCT.md +++ b/docs/CODE_OF_CONDUCT.md @@ -1,11 +1,14 @@ # Code of Conduct + White Whale has an open, friendly, diverse, and welcoming community. We accept and respect everybody equally, but we don't tolerate harassing, trolling, or spamming. We are here to build, not to engage in toxic behavior. ## Our Pledge + We as members, contributors, and leaders pledge to make participation in the community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. All White Whale contributors are required to be respectful towards other members of the team and the community. We expect everybody to act in the best interest of the community and the protocol. ## Conflict resolution + Contributors and community members should always try to solve conflicts communicatively. We encourage everybody to use appropriate communication channels to express their concerns and feelings. These channels include on-chain governance and social media platforms like Twitter and Discord. Always remember to focus on what is best for the community and protocol. TLDR diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 0d87c028..ffd71683 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,10 +1,13 @@ # Security Policies and Procedures + This document outlines security procedures and general policies for the `migaloo-frontend` project. -* [Reporting a Bug](#reporting-a-bug) -* [Disclosure Policy](#disclosure-policy) -* [Suggestions on this Policy](#comments-on-this-policy) + +- [Reporting a Bug](#reporting-a-bug) +- [Disclosure Policy](#disclosure-policy) +- [Suggestions on this Policy](#comments-on-this-policy) ## Reporting a Bug + Security is something we take seriously at White Whale. Thanks for taking the time to improve the security of `migaloo-frontend`. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. Please report security bugs by emailing White Whale at security@whitewhale.money. Do not report it publicly on the GitHub issues tracker. Your report should detail the necessary steps to reproduce the security issue. We will acknowledge your email within 48 hours and send a detailed response indicating the next steps. After the initial reply to your report, we will keep you informed of the progress towards a fix and full announcement and may ask for additional information or guidance. @@ -12,11 +15,14 @@ Please report security bugs by emailing White Whale at security@whitewhale.money Report security bugs in third-party modules to the person or team maintaining the module. ## Disclosure Policy + If we receive a security bug report, we assign it to a primary handler. This person will coordinate the fix and release process, involving the following steps: -* Confirm the problem and determine the affected versions. -* Audit code to find any potentially similar problems. -* Rollout the fixes. + +- Confirm the problem and determine the affected versions. +- Audit code to find any potentially similar problems. +- Rollout the fixes. ## Suggestions on this Policy + If you have suggestions on how to improve this process, please submit a pull request or reach out on [Discord](https://discord.com/invite/tSxyyCWgYX). diff --git a/justfile b/justfile index 584ce846..09caf308 100644 --- a/justfile +++ b/justfile @@ -34,6 +34,7 @@ format: cargo fmt --all find . -type f -iname "*.toml" -print0 | xargs -0 taplo format find . -type f -name '*.sh' -exec shfmt -w {} \; + scripts/utils/format_md.sh # Runs clippy with the a feature flag if provided. lint FEATURE='': diff --git a/packages/white-whale-std/README.md b/packages/white-whale-std/README.md index 9dac1ca6..95de41ba 100644 --- a/packages/white-whale-std/README.md +++ b/packages/white-whale-std/README.md @@ -1,3 +1,3 @@ # White Whale Package -This is a collection of types and helpers commonly used across White Whale protocol. \ No newline at end of file +This is a collection of types and helpers commonly used across White Whale protocol. diff --git a/scripts/README.md b/scripts/README.md index c82b1800..e6406798 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,3 +1,3 @@ # Scripts -Here you can find different scripts used by the White Whale team to streamline certain operations, e.g. contract deployment. \ No newline at end of file +Here you can find different scripts used by the White Whale team to streamline certain operations, e.g. contract deployment. diff --git a/scripts/utils/format_md.sh b/scripts/utils/format_md.sh new file mode 100755 index 00000000..35e65885 --- /dev/null +++ b/scripts/utils/format_md.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o pipefail +command -v shellcheck >/dev/null && shellcheck "$0" + +# Running with -c makes the script only validate instead of editing in place. +op="write" +while getopts c option; do + case "${option}" in + c) op="check" ;; + *) ;; + esac +done + +npx prettier@2.7.1 --$op "./**/*.md" From e26f79d0439024fc91abb1838bb0fefcb31431f8 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 20 May 2024 23:30:36 +0100 Subject: [PATCH 40/51] chore: fix PR comments --- .../schema/bonding-manager.json | 2 +- .../bonding-manager/schema/raw/execute.json | 2 +- .../bonding-manager/src/bonding/commands.rs | 13 ++++--- .../bonding-manager/src/helpers.rs | 4 +-- .../bonding-manager/src/rewards/commands.rs | 19 +++++----- .../bonding-manager/src/state.rs | 2 +- .../epoch-manager/tests/epoch.rs | 35 ++++++++++--------- .../incentive-manager/src/state.rs | 3 +- .../white-whale-std/src/bonding_manager.rs | 3 +- 9 files changed, 40 insertions(+), 43 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 7951283d..c731c146 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -98,7 +98,7 @@ "additionalProperties": false }, { - "description": "Sends withdrawable unbonded tokens to the user.", + "description": "Sends withdrawable assets of the given denom to the user. An asset becomes withdrawable after it has been unbonded and the unbonding period has passed.", "type": "object", "required": [ "withdraw" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index 4df3f63b..78bebfa4 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -37,7 +37,7 @@ "additionalProperties": false }, { - "description": "Sends withdrawable unbonded tokens to the user.", + "description": "Sends withdrawable assets of the given denom to the user. An asset becomes withdrawable after it has been unbonded and the unbonding period has passed.", "type": "object", "required": [ "withdraw" diff --git a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs index c93c7e1a..ca4a1dd5 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bonding/commands.rs @@ -84,6 +84,11 @@ pub(crate) fn bond( GLOBAL.save(deps.storage, &global_index)?; + // first time the user bonds it shouldn't be able to claim rewards until the next epoch. This is + // why we save the last claimed epoch as the current epoch. + // In case the user has already bonded before, it won't be able to bond again without first + // claiming the pending rewards, in which case the last claimed epoch will be updated to the + // current epoch anyway. LAST_CLAIMED_EPOCH.save(deps.storage, &info.sender, ¤t_epoch.epoch.id)?; Ok(Response::default().add_attributes(vec![ @@ -125,12 +130,6 @@ pub(crate) fn unbond( if bonds_by_receiver.is_empty() { Err(ContractError::NothingToUnbond) } else { - // sanity check - ensure!( - bonds_by_receiver.len() == 1usize, - ContractError::AssetMismatch - ); - let mut unbond = bonds_by_receiver[0].clone(); // check if the address has enough bond @@ -192,7 +191,7 @@ pub(crate) fn unbond( } } -/// Withdraws the rewards for the provided address +/// Withdraws the unbonded asset of the given denom for the provided address pub(crate) fn withdraw( deps: DepsMut, address: Addr, diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 2b4d444b..83711b03 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -165,7 +165,7 @@ pub fn swap_coins_to_main_token( coins: Vec, deps: &DepsMut, config: Config, - to_be_distribution_asset: &mut Coin, + distribution_asset: &mut Coin, distribution_denom: &String, messages: &mut Vec, ) -> Result<(), ContractError> { @@ -233,7 +233,7 @@ pub fn swap_coins_to_main_token( // Add the simulate amount received to the distribution_denom amount, if the swap fails this should // also be rolled back - to_be_distribution_asset.amount = to_be_distribution_asset + distribution_asset.amount = distribution_asset .amount .checked_add(simulate_swap_operations_response.amount)?; diff --git a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs index ae0ad7f9..6b7c7082 100644 --- a/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/rewards/commands.rs @@ -29,19 +29,16 @@ pub(crate) fn on_epoch_created( ContractError::Unauthorized ); - let global = GLOBAL.may_load(deps.storage)?; - // This happens only on the very first epoch where Global has not been initialised yet - if global.is_none() { - let initial_global_index = GlobalIndex { + let mut global_index = GLOBAL.load(deps.storage).unwrap_or( + // This happens only on the very first epoch where Global has not been initialised yet + GlobalIndex { epoch_id: current_epoch.id, last_updated: current_epoch.id, ..Default::default() - }; - GLOBAL.save(deps.storage, &initial_global_index)?; - } + }, + ); // Update the global index epoch id - let mut global_index = GLOBAL.load(deps.storage)?; global_index.epoch_id = current_epoch.id; if global_index.bonded_amount == Uint128::zero() { @@ -100,7 +97,7 @@ pub(crate) fn fill_rewards( let mut submessages: Vec = vec![]; // swap non-whale to whale - let mut distribution_denom_in_tx = info + let mut distribution_asset_in_tx = info .funds .iter() .find(|coin| coin.denom.eq(distribution_denom.as_str())) @@ -126,7 +123,7 @@ pub(crate) fn fill_rewards( remnant_coins, &deps, config, - &mut distribution_denom_in_tx, + &mut distribution_asset_in_tx, &distribution_denom, &mut messages, )?; @@ -136,7 +133,7 @@ pub(crate) fn fill_rewards( // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned // If this became an issue we could look at replies instead of the query // The lp_tokens being withdrawn are handled in the reply entry point - fill_upcoming_reward_bucket(deps, distribution_denom_in_tx.clone())?; + fill_upcoming_reward_bucket(deps, distribution_asset_in_tx.clone())?; Ok(Response::default() .add_messages(messages) diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 738de7c5..a085f1f3 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -11,7 +11,7 @@ pub const BONDING_ASSETS_LIMIT: usize = 2; pub const CONFIG: Item = Item::new("config"); /// A monotonically increasing counter to generate unique bond ids. -pub const BOND_COUNTER: Item = Item::new("bond_id_counter"); +pub const BOND_COUNTER: Item = Item::new("bond_counter"); pub const BONDS: IndexedMap = IndexedMap::new( "bonds", BondIndexes { diff --git a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs index 03e6d5e2..6bcb8f78 100644 --- a/contracts/liquidity_hub/epoch-manager/tests/epoch.rs +++ b/contracts/liquidity_hub/epoch-manager/tests/epoch.rs @@ -2,6 +2,7 @@ use cosmwasm_std::from_json; use cosmwasm_std::testing::{mock_env, mock_info}; use epoch_manager::contract::{execute, query}; +use epoch_manager::ContractError; use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochResponse, ExecuteMsg, QueryMsg}; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; use white_whale_std::pool_network::mock_querier::mock_dependencies; @@ -61,20 +62,20 @@ fn create_new_epoch_successfully() { ); } -// #[test] -// fn create_new_epoch_unsuccessfully() { -// let mut deps = mock_dependencies(&[]); -// let info = mock_info("owner", &[]); -// let mut env = mock_env(); -// mock_instantiation(deps.as_mut(), info.clone()).unwrap(); - -// // move time ahead but not enough so the epoch creation fails -// env.block.time = env.block.time.plus_nanos(86300); - -// let msg = ExecuteMsg::CreateEpoch; -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// match err { -// ContractError::CurrentEpochNotExpired => {} -// _ => panic!("should return ContractError::CurrentEpochNotExpired"), -// } -// } +#[test] +fn create_new_epoch_unsuccessfully() { + let mut deps = mock_dependencies(&[]); + let info = mock_info("owner", &[]); + let mut env = mock_env(); + mock_instantiation(deps.as_mut(), info.clone()).unwrap(); + + // move time ahead but not enough so the epoch creation fails + env.block.time = env.block.time.plus_nanos(86300); + + let msg = ExecuteMsg::CreateEpoch; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + match err { + ContractError::CurrentEpochNotExpired => {} + _ => panic!("should return ContractError::CurrentEpochNotExpired"), + } +} diff --git a/contracts/liquidity_hub/incentive-manager/src/state.rs b/contracts/liquidity_hub/incentive-manager/src/state.rs index 245d539f..a27f0378 100644 --- a/contracts/liquidity_hub/incentive-manager/src/state.rs +++ b/contracts/liquidity_hub/incentive-manager/src/state.rs @@ -44,8 +44,7 @@ pub const LAST_CLAIMED_EPOCH: Map<&Addr, EpochId> = Map::new("last_claimed_epoch /// The lp weight history for addresses, including the contract. i.e. how much lp weight an address /// or contract has at a given epoch. /// Key is a tuple of (address, lp_denom, epoch_id), value is the lp weight. -pub const LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = - Map::new("address_lp_weight_history"); +pub const LP_WEIGHT_HISTORY: Map<(&Addr, &str, EpochId), Uint128> = Map::new("lp_weight_history"); /// A monotonically increasing counter to generate unique incentive identifiers. pub const INCENTIVE_COUNTER: Item = Item::new("incentive_counter"); diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index 0259919a..535b0ce7 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -135,7 +135,8 @@ pub enum ExecuteMsg { /// The asset to unbond. asset: Coin, }, - /// Sends withdrawable unbonded tokens to the user. + /// Sends withdrawable assets of the given denom to the user. An asset becomes withdrawable after + /// it has been unbonded and the unbonding period has passed. Withdraw { /// The denom to withdraw. denom: String, From 39b3bc76a6acfe7cf99b46f0014694b1df374bfa Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 20 May 2024 18:00:39 +0100 Subject: [PATCH 41/51] docs: tweak v2 architecture diagram --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b3fe6a1c..55a56329 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,14 @@ The following is the V2 architecture, and a general description of each contract --- title: White Whale V2 --- -flowchart LR +flowchart P[Pool Manager] <--> I[Incentive Manager] -P[Pool Manager] --> B[Bonding Manager] -V[Vault Manager] --> B -B --> E[Epoch Manager] -I --> E +P -.->|swap fees| P +P[Pool Manager] -->|protocol fees| B[Bonding Manager] +V[Vault Manager] -->|protocol fees| B +V -.->|flashloan fees| V +B <-->|on_epoch_created| E[Epoch Manager] +I <-->|on_epoch_created| E click P "https://github.com/White-Whale-Defi-Platform/white-whale-core/tree/release/v2_contracts/contracts/liquidity_hub/pool-manager" "Pool Manager" click V "https://github.com/White-Whale-Defi-Platform/white-whale-core/tree/release/v2_contracts/contracts/liquidity_hub/vault-manager" "Vault Manager" @@ -81,10 +83,9 @@ distributing incentives on pools. Incentive creation is permissionless, meaning The Incentive Manager depends on the Epoch Manager, as incentives are distributed based on epochs. ### Bonding Manager - -The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens -and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends -on the Epoch Manager, as the fee distribution is done based on epochs. +The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens +and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends +on the Epoch Manager, as the rewards distribution is done based on epochs. ### Epoch Manager From ae0591c56d0d92b6d26eec33e3de64e75ffe0de3 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 20 May 2024 15:29:10 +0100 Subject: [PATCH 42/51] docs(epoch-manager): add readme to epoch manager --- .../liquidity_hub/epoch-manager/README.md | 105 +++--------------- 1 file changed, 13 insertions(+), 92 deletions(-) diff --git a/contracts/liquidity_hub/epoch-manager/README.md b/contracts/liquidity_hub/epoch-manager/README.md index 054ea481..a08f08de 100644 --- a/contracts/liquidity_hub/epoch-manager/README.md +++ b/contracts/liquidity_hub/epoch-manager/README.md @@ -1,99 +1,20 @@ -# CosmWasm Starter Pack +# Epoch Manager -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. +The Epoch Manager is a contract which sole purpose is to create epochs on the Migaloo ecosystem. -## Creating a new repo from template +## How it works -Assuming you have a recent version of Rust and Cargo installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: +When the contract is instantiated, a start epoch and the epoch configuration are set up. The epoch configuration defines +the duration of an epoch and when the genesis epoch is, i.e. the first epoch. -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: +Once the genesis epoch is created, after the epoch duration has passed, anyone can create a new epoch by calling the +`CreateEpoch` message. This action will create a new epoch, i.e. increase the epoch id by one, and alert the contracts +that have registered for the hook. -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` +## Epoch Hook -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: +There are two actions that only the owner of the Epoch Manager can call for: `AddHook` and `RemoveHook`. These add a contract +to the `HOOKS` list. -**Latest** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -``` - -For cloning minimal code repo: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -d minimal=true -``` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -``` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. +These contracts must implement the `EpochChangedHookMsg` interface, which is the signature of the message that will be +executed on the hooks when a new epoch is created. The hook contains the current `Epoch`, specifying the id and start_time. From 52bed9159f08439ff444746829ef0461725b5f5f Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 21 May 2024 11:03:55 +0100 Subject: [PATCH 43/51] docs(epoch-manager): add epoch details and hook mechanismm diagram --- .../liquidity_hub/epoch-manager/README.md | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/contracts/liquidity_hub/epoch-manager/README.md b/contracts/liquidity_hub/epoch-manager/README.md index a08f08de..361294c2 100644 --- a/contracts/liquidity_hub/epoch-manager/README.md +++ b/contracts/liquidity_hub/epoch-manager/README.md @@ -1,20 +1,37 @@ # Epoch Manager -The Epoch Manager is a contract which sole purpose is to create epochs on the Migaloo ecosystem. +The Epoch Manager is a contract which sole purpose is to create epochs on the Migaloo ecosystem, acting like a clock for +the other contracts. + +An `Epoch` is a period of time that is defined by the `duration` parameter on `EpochConfig`, and they are used by other +contracts to take timely actions. For example, the Bonding Manager uses epochs to calculate the rewards for the bonders, +while the Incentive Manager uses epochs to calculate the incentive rewards for its users. ## How it works -When the contract is instantiated, a start epoch and the epoch configuration are set up. The epoch configuration defines -the duration of an epoch and when the genesis epoch is, i.e. the first epoch. +The epoch configuration is set up when the contract is instantiated. The epoch configuration defines +the duration of an epoch and when the genesis epoch is gonna take place, i.e. the first epoch. Once the genesis epoch is created, after the epoch duration has passed, anyone can create a new epoch by calling the -`CreateEpoch` message. This action will create a new epoch, i.e. increase the epoch id by one, and alert the contracts -that have registered for the hook. +`CreateEpoch` message. This action will create a new epoch by increasing the epoch id by one, adjust the start time for +the new epoch and alert the contracts that have registered for the hook. ## Epoch Hook -There are two actions that only the owner of the Epoch Manager can call for: `AddHook` and `RemoveHook`. These add a contract -to the `HOOKS` list. +There are two actions that only the owner of the Epoch Manager can execute: `AddHook` and `RemoveHook`. These add or +remove a contract to the `HOOKS` list. These contracts must implement the `EpochChangedHookMsg` interface, which is the signature of the message that will be executed on the hooks when a new epoch is created. The hook contains the current `Epoch`, specifying the id and start_time. + +```mermaid +--- +title: Epoch Manager Hook Mechanism +--- +flowchart +E[Epoch Manager] -->|on_epoch_created| C1[Contract 1] +E -.->|CreateEpoch| E +E -->|on_epoch_created| C2[Contract 2] +E -->|on_epoch_created| N[...] +E -->|on_epoch_created| CN[Contract N] +``` \ No newline at end of file From 15896a25432893fc8e6ace2dbfbbfaea851292ff Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Tue, 21 May 2024 12:29:44 +0100 Subject: [PATCH 44/51] docs(incentive-manager): add readme to incentive manager Co-authored-by: 0xFable <0xfable@protonmail.com> --- .../liquidity_hub/incentive-manager/README.md | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/contracts/liquidity_hub/incentive-manager/README.md b/contracts/liquidity_hub/incentive-manager/README.md index fda84ecb..3a39f183 100644 --- a/contracts/liquidity_hub/incentive-manager/README.md +++ b/contracts/liquidity_hub/incentive-manager/README.md @@ -1,4 +1,93 @@ # Incentive Manager -The Incentive Manager is the V2 iteration of the original incentives. This is a monolithic contract that handles all +The Incentive Manager is the V2 iteration of the original incentives system; this is a monolithic contract that handles all the incentives-related logic. + +## How it works + +The Incentives system is now completely encapsulated within one contract called the Incentive Manager. The Incentive +Manager has two main concepts; an `Incentive`, defined as a reward to be distributed and a `Position`, defined as a +user's liquidity in a pool locked in the contract. + +Users of the Liquidity Hub, when providing liquidity, can opt to lock their LP shares which will in turn send them to +the Incentive Manager until they are unlocked. + +### Incentives + +Creating incentives is permissionless, and incentives can be perpetual. This means they can be expanded forever. Anyone +can create an incentive by calling the `ManageIncentive` message with the `IncentiveAction::Fill` action and paying the +incentive creation fee, which is sent to the Bonding Manager. + +Users can decide to provide an identifier, which they can later use to top up or close the incentive. If no identifier is +provided, the contract will generate one. + +#### Topping up an Incentive + +To top up an incentive, the owner of the incentive must call `ManageIncentive` with the `IncentiveAction::Fill` action. +The user must provide the same identifier as the original incentive. The incentive can only be topped up with the same +token as the original incentive, and the amount must be a multiple of the original incentive's amount. + +#### Closing an Incentive + +To close an incentive, the owner of the incentive or the owner of the contract must call `ManageIncentive` with the +`IncentiveAction::Close` action with the identifier of the incentive to be closed. The incentive will be closed, and the +remaining tokens will be sent to the owner of the incentive. + +#### Reward Distribution + +Incentive rewards are distributed every epoch, which is created by the Epoch Manager. Whenever an epoch is created; the +Incentive Manager gets called via the `EpochChangedHook` hook, alerting the contract that a new epoch has been created. +The contract will then take snapshots for every LP token in the contract and save it in the `LP_WEIGHT_HISTORY` map for +the current epoch. That helps to calculate the rewards when users claim them. + +The maximum number of concurrent incentives for a given LP denom is defined when the contract is instantiated, and it is +stored in the config as `max_concurrent_incentives`. + + +### Positions + +Positions can be created, expanded (topped up), or withdrawn. This is done via the `ManagePosition` +message, followed by the desired action, i.e. `PositionAction::Fill`, `PositionAction::Close` or `PositionAction::Withdraw`. +When a user creates a position, it must provide an unlocking duration. The unlocking duration is the time it takes in +seconds to unlock the position, which is necessary to withdraw the LP tokens from the contract. + +#### Topping up a Position + +When a user creates a position, the LP tokens are locked in the contract. The user can't withdraw them until the unlocking +duration is complete. To expand a position, the user must call `ManagePosition` with the `PositionAction::Fill` action +using the same position identifier as the original position. In this case, since it's considered to be the same position, +any changes in the unlocking duration parameter passed along with the `PositionAction::Fill` action will be ignored. +Instead, the one in the original position will be used. + +If a user doesn't provide an identifier when creating a position, the contract will generate one. + +The minimum unlocking duration is 1 day, and the maximum is 365 days. + +#### Closing a Position + +Closing a position is done by calling `ManagePosition` with the `PositionAction::Close` action. The user must provide the +identifier of the position to be closed. Once this action is triggered, the `Position.open` state is set to false, and +`expiring_at` is set to the block height after which the position will be able to be withdrawn. + +#### Withdrawing a Position + +Once the unlocking duration is complete, the user can withdraw the LP tokens from the contract by calling the `ManagePosition` +with the `PositionAction::Withdraw` action. Alternatively, if the user doesn't want to wait for the unlocking duration to +complete, it is possible to do an emergency withdrawal by passing `true` on the `emergency_unlock` parameter. This will +unlock and withdraw the position immediately, but the user will pay a penalty fee that will go the Bonding Manager and +distributed to the bonders. + +Once the user closes and withdraws the position, they receive their LP tokens back. + +### Claiming Incentive Rewards + +Users can claim incentive rewards from active incentives for their LP tokens, only if they have a position in the +contract. Users can only claim rewards for future epochs, i.e. after the epoch in which the position was created. + +Incentive rewards are distributed based on the user's share of the total LP tokens in the contract. So if there's a total +of 100 LP tokens in the contract, and a user has 10 LP tokens, the user will receive 10% of the rewards for that epoch, +for that given incentive. + +To claim rewards, the user must call the `Claim` message. Once that's done, the contract will save the epoch in which the +claim was made in `LAST_CLAIMED_EPOCH`, and will sync the user's LP weight history saved in `LP_WEIGHT_HISTORY`. This helps +computing the rewards for the user. From daed937a1c599c2c313ff258f36b015993247887 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Tue, 21 May 2024 12:30:16 +0100 Subject: [PATCH 45/51] docs(vault-manager): add readme for vault manager Co-authored-by: 0xFable <0xfable@protonmail.com> --- .../liquidity_hub/vault-manager/README.md | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/contracts/liquidity_hub/vault-manager/README.md b/contracts/liquidity_hub/vault-manager/README.md index ee739335..0af25eaf 100644 --- a/contracts/liquidity_hub/vault-manager/README.md +++ b/contracts/liquidity_hub/vault-manager/README.md @@ -1,4 +1,60 @@ # Vault Manager -The Vault Manager is the V2 iteration of the original WW vault network. This is a monolithic contract that handles all -the vaults and flashloans in the White Whale DEX. +The Vault Manager is a contract handling a collection of single asset vaults that bots use to access flashloan capital for +arbitrage, liquidation, and other DeFi uses. By using the Flash Loan Vaults, arbitrage and liquidations happen locally +in a capital-efficient manner and without capital requirements. That is, each arbitrageur or liquidator will no longer +need their own capital on each local chain waiting idly to arb or to liquidate because they can access a flash loan for +their capital needs. When an arbitrage opportunity arises, an arbitrageur takes a flash loan, arbs the local dex price +versus a Migaloo pool, and then pays back the loan plus the flash loan fee. The arbitrageur then keeps the profit without +having used any of their own capital. + +Depositors of tokens into flash loan vaults benefit from fees paid when their vault is accessed for flash loans; the +greater the volume, the more fees generated. Flash loan vaults are a great source of yield with no impermanent loss. + +## How it works + +The following is a high-level overview of how the Vault Manager works. It touches on some technical details, assisting +developers in understanding the contract's inner workings, while also providing a general understanding of the contract's +functionality, so a regular user can understand how to interact with it. + +### Vault Creation + +Creating vaults is a simple and permissionless process. A user can call the `CreateVault` message, with the desired vault +parameters together with the vault creation fee. The vault creation fee is a protocol fee that is sent to the Bonding +Manager. + +Vaults are distinguished by unique IDs, meaning there can be multiple vaults for the same asset, e.i. multiple whale +vaults, as long as they have different identifiers. Vaults cannot be removed or updated once created, so it is important +to get the parameters right from the start. + +### Deposits + +Users can deposit into a vault at any time by calling the `Deposit` message, containing the vault identifier they wish to +deposit assets into. Each vault is stored in the state of the contract, and it keeps track of the assets deposited in it. + +When a user deposits into a vault, the assets are transferred to the vault's balance, while the user receives an LP token +representing the share of the vault they own. The LP token can be used to withdraw the assets from the vault at any time. + +### Withdrawals + +Users can withdraw from a vault at any time by calling the `Withdraw` message, sending the amount of LP tokens they got +when depositing assets into a vault. Since each `Vault` has a unique LP token there's no need to specify a vault +identifier when withdrawing as the contract can find the Vault the user deposited into by analyzing the LP token sent. + +The LP tokens are burned and the original assets are transferred back to the user. + +### Flash loans + +Flash loans are a powerful tool that allows bots and users to borrow assets from the vault without any collateral, with +the condition that the assets plus the flash loan fees are returned within the same transaction, otherwise the transaction +is reverted as if nothing had happened. + +A flash loan can be taken by calling the `FlashLoan` message, with the desired amount, vault identifier and the payload. +The payload is a list of messages that will be executed in the same transaction, and it doesn't need to include the "payback" +transaction as it is handled by the contract. + +When a flash loan is taken, a boolean in the state is set to true on `ONGOING_FLASHLOAN`, so the funds can't be used to +be deposited back into a vault or to take another flash loan. After the payload is executed, the `CallbackMsg::AfterFlashloan` +is called. This makes sure the funds are back in the vault plus the fees. The profit made from the payload operations is +sent back to the originator of the flash loan. The Bonding Manager receives the protocol fees via the `FillRewards` message +and the users that have deposited into the vault where the flash loan was taken from receive the flash loan fees. From b7ea839a00d03875acd109d9f2e546e3ee3b6347 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Tue, 21 May 2024 13:38:51 +0100 Subject: [PATCH 46/51] docs(bonding-manager): add diagram of reward distribution (#359) --- .../liquidity_hub/bonding-manager/README.md | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/contracts/liquidity_hub/bonding-manager/README.md b/contracts/liquidity_hub/bonding-manager/README.md index f4fc2c27..8177d971 100644 --- a/contracts/liquidity_hub/bonding-manager/README.md +++ b/contracts/liquidity_hub/bonding-manager/README.md @@ -11,18 +11,34 @@ functionality so a regular user can understand how to interact with it. ### Rewards -Users must bond WHALE LSDs to the Bonding Manager to start earning rewards. Every time there's a swap on the pool manager -or a flashloan taken on the vault manager, a fee is collected and sent to the Bonding Manager via the `FillRewards` message. -The rewards collected within an epoch are temporarily stored in the `UPCOMING_REWARD_BUCKET`, which is then distributed to bonders -when the next epoch is created via the Epoch Manager contract, which has a hook to the Bonding Manager calling `EpochChangedHook`. +Users must bond WHALE LSDs to the Bonding Manager to start earning rewards generated by the system, i.e. pools and vaults. +Every time there's a swap on the pool manager or a flashloan taken on the vault manager, a fee is collected and sent to +the Bonding Manager via the `FillRewards` message. The rewards collected within an epoch are temporarily stored in the +`UPCOMING_REWARD_BUCKET`, which is then distributed to bonders when the next epoch is created via the Epoch Manager +contract, which has a hook to the Bonding Manager calling `EpochChangedHook`. -When `EpochChangedHook` is called, a new reward bucket is created from the rewards collected in the previous epoch +When `EpochChangedHook` is called, a new `RewardBucket` is created from the rewards collected in the previous epoch (stored in `UPCOMING_REWARD_BUCKET`), with a snapshot of the global index stored in `GLOBAL`. The `GlobalIndex` struct contains information about the total bonded WHALE LSDs, the epoch id it of the snapshot, when (epoch id) was it last updated, and most importantly, the global weight. The weight is a formula based on how many assets are in the contract and a time factor, that rewards users that have bonded the longer. The weight is used to calculate the rewards for each bonder when the claim takes place. +```mermaid +--- +title: Bonding Manager Reward Distribution +--- +flowchart + P[Pool Manager] -->|FillRewards| B[Bonding Manager] + V[Vault Manager] -->|FillRewards| B + E[Epoch Manager] ==>|on_epoch_created| B + B[Bonding Manager] ==>|on_epoch_created| B1[Reward Bucket 21] + B ---|Upcoming Reward Bucket| B + B -.-> B2[Reward Bucket 20] + B -.-> N[...] + B -.-> BN[Reward Bucket 1] +``` + ### Bonding and Withdrawing The bonding process is simple. Users call the `Bond` message passing the amount of WHALE LSD they want to bond. This is From 2fda9be9d423132199c44e91d721e0b0d0bd48c5 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Tue, 21 May 2024 13:47:05 +0100 Subject: [PATCH 47/51] docs(pool-manager): add readme to pool manager (#356) --- .../liquidity_hub/pool-manager/README.md | 155 +++++------------- .../liquidity_hub/pool-manager/ROUTER.md | 19 --- 2 files changed, 40 insertions(+), 134 deletions(-) delete mode 100644 contracts/liquidity_hub/pool-manager/ROUTER.md diff --git a/contracts/liquidity_hub/pool-manager/README.md b/contracts/liquidity_hub/pool-manager/README.md index ff8bb7cc..6b7af6b0 100644 --- a/contracts/liquidity_hub/pool-manager/README.md +++ b/contracts/liquidity_hub/pool-manager/README.md @@ -1,132 +1,57 @@ -# CosmWasm Starter Pack +# Pool Manager -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. +The Pool Manager is a contract that handles pools in the Migaloo DEX. -# Architecture +Pools can contain two or three assets and are used to provide liquidity to the DEX. The Pool Manager is responsible for +creating pools and handling swaps, whether they are single or multi-hop operations. -```mermaid -graph LR +## How it works -subgraph WHALE Dex -subgraph Pool Manager - Admin --> |Can Create| Pairs - Users --> |Can Create for X Fee| Pairs --> | Stored In| Contract - Contract --> |Deploys either a native or cw20 based| Liquidity_Token - Users --> |Can Provide Liquidity to | Contract --> |Mint to User| Liquidity_Token - Users --> |Can Withdraw Liquidity from | Contract --> |Burn provided | Liquidity_Token --> |Send refund assets to| Users +The following is a high-level overview of how the Pool Manager works. It touches on some technical details, assisting +developers in understanding the contract's inner workings while also providing a general understanding of the contract's +functionality, so a regular user can understand how to interact with it. +### Pool Creation +Creating pools is a simple and permissionless process. A user can call the `CreatePool` message, with the desired pool +parameters such as asset denoms, fees, and pool type among others, together with the pool creation fee. The pool creation +fee is a protocol fee that is sent to the Bonding Manager via the `FillRewards` message. There can be multiple pools +for the same asset pair, though each pool must have a unique identifier. Pools cannot be removed or updated once +created, so it is important to get the parameters right from the start. - end +The liquidity in a given pool is tracked with LP tokens, which are minted via the Token Factory module by the Pool Manager. +These tokens represent the user's share of a pool's liquidity, and they can be used to redeem the assets in the pool. -end +Pool information is stored in the `POOLS` map, containing information such as the asset denoms and decimals, the LP denom, +the assets in the pool (balance), the pool type and pool fees. -``` +A pool can be of two types: `ConstantProduct` (xyk) or `StableSwap`. The `ConstantProduct` type is suitable for assets that +may have varying values and are not intended to be equivalent. The `StableSwap` type is suitable for assets that are +meant to be the same and whose values should be approximately the same, such as stablecoins. -### Todo work +### Deposits and Withdrawals -- [x] Create a pool manager contract -- [x] Can create a pair -- [x] Can provide liquidity -- [x] Can withdraw liquidity -- [x] Can swap assets -- [ ] Tests for all the above ported over -- [ ] Tests for Swap Operations -- [ ] Tests for swap routes -- [ ] +Users can deposit and withdraw assets from the pools at any time. To deposit, users must call the `ProvideLiquidity` +message, together with the pool identifier and the assets to deposit among other parameters. For pools with two assets, +it is possible to provide liquidity with a single asset. The Pool Manager will swap half of the provided asset for the +other asset in the pool, ensuring the pool's balance is kept in check. -## Creating a new repo from template +Once the user has provided liquidity, they will receive LP tokens in return proportional to the amount of liquidity +provided. -Assuming you have a recent version of Rust and Cargo installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: +To withdraw liquidity, users must call the `WithdrawLiquidity` message, with the pool identifier together with the LP +token to redeem the assets. The Pool Manager will burn the LP tokens and send the corresponding assets to the user, +updating the pool's balance accordingly. -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: +### Swaps -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` +Swaps are the main feature of the Pool Manager. Users can swap assets from one pool to another by using the `Swap` message. +If the swap is a single-hop operation, the Pool Manager will perform the swap directly. If the swap is a multi-hop operation, +the `ExecuteSwapOperations` message should be used instead, providing the route to follow for the swap to be executed +successfully. -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: +After a swap takes place, the pool's balances are updated, and the fees are collected. The Bonding Manager receives the +protocol fee via the `FillRewards` message, while the swap fee remains in the pool to benefit the LP token holders, +increasing the pool's liquidity and thus the LP token value. -**Latest** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -``` - -For cloning minimal code repo: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -d minimal=true -``` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -``` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. +On Osmosis, there's an additional fee that is sent to the Osmosis community pool. diff --git a/contracts/liquidity_hub/pool-manager/ROUTER.md b/contracts/liquidity_hub/pool-manager/ROUTER.md deleted file mode 100644 index f816ee67..00000000 --- a/contracts/liquidity_hub/pool-manager/ROUTER.md +++ /dev/null @@ -1,19 +0,0 @@ -# Pool Manager router - -Previously in the pool-network repo a router contract is used to perform multihop swaps which involve more than 1 hop or route to complete a swap. -This functionality is needed in the pool manager but the implementation needs to be different. -In the pool manager there are many pairs and its possible to have multiple pairs with the same assets. For this reason we can't deterministically evaluate a pair key from just asstes. Instead a pool_idenitifer is used to identify a pair and each pair has one. This is what is used to specify a pair in the pool manager. - -A multihop swap with this pair manager contract could look like the below - -``` -routes: - - pool_id: '960' - token_out_denom: uosmo - - pool_id: '2' - token_out_denom: uion - -``` - -The above route would swap from a given route to uosmo and then from uosmo to uion. -Becuase the route is specified by pool_id we can have multiple routes with the same assets but when providing a pool ID and the asset we want we can determine the asset its trading against. From 78ca31f9a0bb4cc0b672d18ed08b890280e930e6 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Thu, 23 May 2024 17:19:34 +0100 Subject: [PATCH 48/51] docs(epopoch-manager): add missing docummentation to structs and enum defitions (#362) --- README.md | 5 +- .../liquidity_hub/bonding-manager/README.md | 10 +- .../schema/bonding-manager.json | 1 + .../bonding-manager/schema/raw/execute.json | 1 + .../liquidity_hub/epoch-manager/README.md | 18 +-- .../epoch-manager/src/contract.rs | 6 +- .../liquidity_hub/incentive-manager/README.md | 57 +++++---- .../liquidity_hub/pool-manager/README.md | 34 +++--- .../liquidity_hub/vault-manager/README.md | 48 ++++---- .../src/epoch_manager/epoch_manager.rs | 114 ++++++++++-------- 10 files changed, 157 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 55a56329..a2492fdf 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,9 @@ distributing incentives on pools. Incentive creation is permissionless, meaning The Incentive Manager depends on the Epoch Manager, as incentives are distributed based on epochs. ### Bonding Manager -The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens -and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends + +The Bonding Manager is the contract that manages the bonding in the protocol. It is responsible for bonding eligible tokens +and distributing the fees generated by the pools and vaults among the users that bond tokens. The Bonding Manager depends on the Epoch Manager, as the rewards distribution is done based on epochs. ### Epoch Manager diff --git a/contracts/liquidity_hub/bonding-manager/README.md b/contracts/liquidity_hub/bonding-manager/README.md index 8177d971..9f8d531b 100644 --- a/contracts/liquidity_hub/bonding-manager/README.md +++ b/contracts/liquidity_hub/bonding-manager/README.md @@ -11,10 +11,10 @@ functionality so a regular user can understand how to interact with it. ### Rewards -Users must bond WHALE LSDs to the Bonding Manager to start earning rewards generated by the system, i.e. pools and vaults. -Every time there's a swap on the pool manager or a flashloan taken on the vault manager, a fee is collected and sent to -the Bonding Manager via the `FillRewards` message. The rewards collected within an epoch are temporarily stored in the -`UPCOMING_REWARD_BUCKET`, which is then distributed to bonders when the next epoch is created via the Epoch Manager +Users must bond WHALE LSDs to the Bonding Manager to start earning rewards generated by the system, i.e. pools and vaults. +Every time there's a swap on the pool manager or a flashloan taken on the vault manager, a fee is collected and sent to +the Bonding Manager via the `FillRewards` message. The rewards collected within an epoch are temporarily stored in the +`UPCOMING_REWARD_BUCKET`, which is then distributed to bonders when the next epoch is created via the Epoch Manager contract, which has a hook to the Bonding Manager calling `EpochChangedHook`. When `EpochChangedHook` is called, a new `RewardBucket` is created from the rewards collected in the previous epoch @@ -28,7 +28,7 @@ claim takes place. --- title: Bonding Manager Reward Distribution --- -flowchart +flowchart P[Pool Manager] -->|FillRewards| B[Bonding Manager] V[Vault Manager] -->|FillRewards| B E[Epoch Manager] ==>|on_epoch_created| B diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index c731c146..3cf60854 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -297,6 +297,7 @@ "type": "string" }, "Epoch": { + "description": "The epoch definition.", "type": "object", "required": [ "id", diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index 78bebfa4..43d4f285 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -236,6 +236,7 @@ "type": "string" }, "Epoch": { + "description": "The epoch definition.", "type": "object", "required": [ "id", diff --git a/contracts/liquidity_hub/epoch-manager/README.md b/contracts/liquidity_hub/epoch-manager/README.md index 361294c2..ed393828 100644 --- a/contracts/liquidity_hub/epoch-manager/README.md +++ b/contracts/liquidity_hub/epoch-manager/README.md @@ -1,27 +1,27 @@ # Epoch Manager -The Epoch Manager is a contract which sole purpose is to create epochs on the Migaloo ecosystem, acting like a clock for -the other contracts. +The Epoch Manager is a contract which sole purpose is to create epochs on the Migaloo ecosystem, acting like a clock for +the other contracts. -An `Epoch` is a period of time that is defined by the `duration` parameter on `EpochConfig`, and they are used by other +An `Epoch` is a period of time that is defined by the `duration` parameter on `EpochConfig`, and they are used by other contracts to take timely actions. For example, the Bonding Manager uses epochs to calculate the rewards for the bonders, while the Incentive Manager uses epochs to calculate the incentive rewards for its users. ## How it works -The epoch configuration is set up when the contract is instantiated. The epoch configuration defines +The epoch configuration is set up when the contract is instantiated. The epoch configuration defines the duration of an epoch and when the genesis epoch is gonna take place, i.e. the first epoch. -Once the genesis epoch is created, after the epoch duration has passed, anyone can create a new epoch by calling the -`CreateEpoch` message. This action will create a new epoch by increasing the epoch id by one, adjust the start time for +Once the genesis epoch is created, after the epoch duration has passed, anyone can create a new epoch by calling the +`CreateEpoch` message. This action will create a new epoch by increasing the epoch id by one, adjust the start time for the new epoch and alert the contracts that have registered for the hook. ## Epoch Hook -There are two actions that only the owner of the Epoch Manager can execute: `AddHook` and `RemoveHook`. These add or +There are two actions that only the owner of the Epoch Manager can execute: `AddHook` and `RemoveHook`. These add or remove a contract to the `HOOKS` list. -These contracts must implement the `EpochChangedHookMsg` interface, which is the signature of the message that will be +These contracts must implement the `EpochChangedHookMsg` interface, which is the signature of the message that will be executed on the hooks when a new epoch is created. The hook contains the current `Epoch`, specifying the id and start_time. ```mermaid @@ -34,4 +34,4 @@ E -.->|CreateEpoch| E E -->|on_epoch_created| C2[Contract 2] E -->|on_epoch_created| N[...] E -->|on_epoch_created| CN[Contract N] -``` \ No newline at end of file +``` diff --git a/contracts/liquidity_hub/epoch-manager/src/contract.rs b/contracts/liquidity_hub/epoch-manager/src/contract.rs index 935f5fe6..7158d96f 100644 --- a/contracts/liquidity_hub/epoch-manager/src/contract.rs +++ b/contracts/liquidity_hub/epoch-manager/src/contract.rs @@ -78,10 +78,10 @@ pub fn execute( #[entry_point] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => Ok(to_json_binary(&queries::query_config(deps)?)?), - QueryMsg::CurrentEpoch {} => Ok(to_json_binary(&queries::query_current_epoch(deps)?)?), + QueryMsg::Config => Ok(to_json_binary(&queries::query_config(deps)?)?), + QueryMsg::CurrentEpoch => Ok(to_json_binary(&queries::query_current_epoch(deps)?)?), QueryMsg::Epoch { id } => Ok(to_json_binary(&queries::query_epoch(deps, id)?)?), - QueryMsg::Hooks {} => Ok(to_json_binary(&queries::query_hooks(deps)?)?), + QueryMsg::Hooks => Ok(to_json_binary(&queries::query_hooks(deps)?)?), QueryMsg::Hook { hook } => Ok(to_json_binary(&queries::query_hook(deps, hook)?)?), } } diff --git a/contracts/liquidity_hub/incentive-manager/README.md b/contracts/liquidity_hub/incentive-manager/README.md index 3a39f183..e83beb12 100644 --- a/contracts/liquidity_hub/incentive-manager/README.md +++ b/contracts/liquidity_hub/incentive-manager/README.md @@ -5,58 +5,57 @@ the incentives-related logic. ## How it works -The Incentives system is now completely encapsulated within one contract called the Incentive Manager. The Incentive -Manager has two main concepts; an `Incentive`, defined as a reward to be distributed and a `Position`, defined as a +The Incentives system is now completely encapsulated within one contract called the Incentive Manager. The Incentive +Manager has two main concepts; an `Incentive`, defined as a reward to be distributed and a `Position`, defined as a user's liquidity in a pool locked in the contract. -Users of the Liquidity Hub, when providing liquidity, can opt to lock their LP shares which will in turn send them to +Users of the Liquidity Hub, when providing liquidity, can opt to lock their LP shares which will in turn send them to the Incentive Manager until they are unlocked. ### Incentives Creating incentives is permissionless, and incentives can be perpetual. This means they can be expanded forever. Anyone -can create an incentive by calling the `ManageIncentive` message with the `IncentiveAction::Fill` action and paying the -incentive creation fee, which is sent to the Bonding Manager. +can create an incentive by calling the `ManageIncentive` message with the `IncentiveAction::Fill` action and paying the +incentive creation fee, which is sent to the Bonding Manager. Users can decide to provide an identifier, which they can later use to top up or close the incentive. If no identifier is provided, the contract will generate one. #### Topping up an Incentive -To top up an incentive, the owner of the incentive must call `ManageIncentive` with the `IncentiveAction::Fill` action. -The user must provide the same identifier as the original incentive. The incentive can only be topped up with the same +To top up an incentive, the owner of the incentive must call `ManageIncentive` with the `IncentiveAction::Fill` action. +The user must provide the same identifier as the original incentive. The incentive can only be topped up with the same token as the original incentive, and the amount must be a multiple of the original incentive's amount. #### Closing an Incentive -To close an incentive, the owner of the incentive or the owner of the contract must call `ManageIncentive` with the +To close an incentive, the owner of the incentive or the owner of the contract must call `ManageIncentive` with the `IncentiveAction::Close` action with the identifier of the incentive to be closed. The incentive will be closed, and the remaining tokens will be sent to the owner of the incentive. #### Reward Distribution -Incentive rewards are distributed every epoch, which is created by the Epoch Manager. Whenever an epoch is created; the -Incentive Manager gets called via the `EpochChangedHook` hook, alerting the contract that a new epoch has been created. -The contract will then take snapshots for every LP token in the contract and save it in the `LP_WEIGHT_HISTORY` map for +Incentive rewards are distributed every epoch, which is created by the Epoch Manager. Whenever an epoch is created; the +Incentive Manager gets called via the `EpochChangedHook` hook, alerting the contract that a new epoch has been created. +The contract will then take snapshots for every LP token in the contract and save it in the `LP_WEIGHT_HISTORY` map for the current epoch. That helps to calculate the rewards when users claim them. The maximum number of concurrent incentives for a given LP denom is defined when the contract is instantiated, and it is stored in the config as `max_concurrent_incentives`. - ### Positions -Positions can be created, expanded (topped up), or withdrawn. This is done via the `ManagePosition` +Positions can be created, expanded (topped up), or withdrawn. This is done via the `ManagePosition` message, followed by the desired action, i.e. `PositionAction::Fill`, `PositionAction::Close` or `PositionAction::Withdraw`. -When a user creates a position, it must provide an unlocking duration. The unlocking duration is the time it takes in +When a user creates a position, it must provide an unlocking duration. The unlocking duration is the time it takes in seconds to unlock the position, which is necessary to withdraw the LP tokens from the contract. #### Topping up a Position -When a user creates a position, the LP tokens are locked in the contract. The user can't withdraw them until the unlocking -duration is complete. To expand a position, the user must call `ManagePosition` with the `PositionAction::Fill` action +When a user creates a position, the LP tokens are locked in the contract. The user can't withdraw them until the unlocking +duration is complete. To expand a position, the user must call `ManagePosition` with the `PositionAction::Fill` action using the same position identifier as the original position. In this case, since it's considered to be the same position, -any changes in the unlocking duration parameter passed along with the `PositionAction::Fill` action will be ignored. +any changes in the unlocking duration parameter passed along with the `PositionAction::Fill` action will be ignored. Instead, the one in the original position will be used. If a user doesn't provide an identifier when creating a position, the contract will generate one. @@ -65,29 +64,29 @@ The minimum unlocking duration is 1 day, and the maximum is 365 days. #### Closing a Position -Closing a position is done by calling `ManagePosition` with the `PositionAction::Close` action. The user must provide the -identifier of the position to be closed. Once this action is triggered, the `Position.open` state is set to false, and +Closing a position is done by calling `ManagePosition` with the `PositionAction::Close` action. The user must provide the +identifier of the position to be closed. Once this action is triggered, the `Position.open` state is set to false, and `expiring_at` is set to the block height after which the position will be able to be withdrawn. #### Withdrawing a Position -Once the unlocking duration is complete, the user can withdraw the LP tokens from the contract by calling the `ManagePosition` -with the `PositionAction::Withdraw` action. Alternatively, if the user doesn't want to wait for the unlocking duration to -complete, it is possible to do an emergency withdrawal by passing `true` on the `emergency_unlock` parameter. This will -unlock and withdraw the position immediately, but the user will pay a penalty fee that will go the Bonding Manager and +Once the unlocking duration is complete, the user can withdraw the LP tokens from the contract by calling the `ManagePosition` +with the `PositionAction::Withdraw` action. Alternatively, if the user doesn't want to wait for the unlocking duration to +complete, it is possible to do an emergency withdrawal by passing `true` on the `emergency_unlock` parameter. This will +unlock and withdraw the position immediately, but the user will pay a penalty fee that will go the Bonding Manager and distributed to the bonders. Once the user closes and withdraws the position, they receive their LP tokens back. ### Claiming Incentive Rewards -Users can claim incentive rewards from active incentives for their LP tokens, only if they have a position in the -contract. Users can only claim rewards for future epochs, i.e. after the epoch in which the position was created. +Users can claim incentive rewards from active incentives for their LP tokens, only if they have a position in the +contract. Users can only claim rewards for future epochs, i.e. after the epoch in which the position was created. -Incentive rewards are distributed based on the user's share of the total LP tokens in the contract. So if there's a total -of 100 LP tokens in the contract, and a user has 10 LP tokens, the user will receive 10% of the rewards for that epoch, +Incentive rewards are distributed based on the user's share of the total LP tokens in the contract. So if there's a total +of 100 LP tokens in the contract, and a user has 10 LP tokens, the user will receive 10% of the rewards for that epoch, for that given incentive. -To claim rewards, the user must call the `Claim` message. Once that's done, the contract will save the epoch in which the -claim was made in `LAST_CLAIMED_EPOCH`, and will sync the user's LP weight history saved in `LP_WEIGHT_HISTORY`. This helps +To claim rewards, the user must call the `Claim` message. Once that's done, the contract will save the epoch in which the +claim was made in `LAST_CLAIMED_EPOCH`, and will sync the user's LP weight history saved in `LP_WEIGHT_HISTORY`. This helps computing the rewards for the user. diff --git a/contracts/liquidity_hub/pool-manager/README.md b/contracts/liquidity_hub/pool-manager/README.md index 6b7af6b0..dc8395a6 100644 --- a/contracts/liquidity_hub/pool-manager/README.md +++ b/contracts/liquidity_hub/pool-manager/README.md @@ -15,43 +15,43 @@ functionality, so a regular user can understand how to interact with it. Creating pools is a simple and permissionless process. A user can call the `CreatePool` message, with the desired pool parameters such as asset denoms, fees, and pool type among others, together with the pool creation fee. The pool creation -fee is a protocol fee that is sent to the Bonding Manager via the `FillRewards` message. There can be multiple pools -for the same asset pair, though each pool must have a unique identifier. Pools cannot be removed or updated once +fee is a protocol fee that is sent to the Bonding Manager via the `FillRewards` message. There can be multiple pools +for the same asset pair, though each pool must have a unique identifier. Pools cannot be removed or updated once created, so it is important to get the parameters right from the start. -The liquidity in a given pool is tracked with LP tokens, which are minted via the Token Factory module by the Pool Manager. +The liquidity in a given pool is tracked with LP tokens, which are minted via the Token Factory module by the Pool Manager. These tokens represent the user's share of a pool's liquidity, and they can be used to redeem the assets in the pool. -Pool information is stored in the `POOLS` map, containing information such as the asset denoms and decimals, the LP denom, +Pool information is stored in the `POOLS` map, containing information such as the asset denoms and decimals, the LP denom, the assets in the pool (balance), the pool type and pool fees. -A pool can be of two types: `ConstantProduct` (xyk) or `StableSwap`. The `ConstantProduct` type is suitable for assets that -may have varying values and are not intended to be equivalent. The `StableSwap` type is suitable for assets that are +A pool can be of two types: `ConstantProduct` (xyk) or `StableSwap`. The `ConstantProduct` type is suitable for assets that +may have varying values and are not intended to be equivalent. The `StableSwap` type is suitable for assets that are meant to be the same and whose values should be approximately the same, such as stablecoins. ### Deposits and Withdrawals -Users can deposit and withdraw assets from the pools at any time. To deposit, users must call the `ProvideLiquidity` -message, together with the pool identifier and the assets to deposit among other parameters. For pools with two assets, -it is possible to provide liquidity with a single asset. The Pool Manager will swap half of the provided asset for the +Users can deposit and withdraw assets from the pools at any time. To deposit, users must call the `ProvideLiquidity` +message, together with the pool identifier and the assets to deposit among other parameters. For pools with two assets, +it is possible to provide liquidity with a single asset. The Pool Manager will swap half of the provided asset for the other asset in the pool, ensuring the pool's balance is kept in check. -Once the user has provided liquidity, they will receive LP tokens in return proportional to the amount of liquidity +Once the user has provided liquidity, they will receive LP tokens in return proportional to the amount of liquidity provided. -To withdraw liquidity, users must call the `WithdrawLiquidity` message, with the pool identifier together with the LP -token to redeem the assets. The Pool Manager will burn the LP tokens and send the corresponding assets to the user, +To withdraw liquidity, users must call the `WithdrawLiquidity` message, with the pool identifier together with the LP +token to redeem the assets. The Pool Manager will burn the LP tokens and send the corresponding assets to the user, updating the pool's balance accordingly. ### Swaps Swaps are the main feature of the Pool Manager. Users can swap assets from one pool to another by using the `Swap` message. -If the swap is a single-hop operation, the Pool Manager will perform the swap directly. If the swap is a multi-hop operation, -the `ExecuteSwapOperations` message should be used instead, providing the route to follow for the swap to be executed -successfully. +If the swap is a single-hop operation, the Pool Manager will perform the swap directly. If the swap is a multi-hop operation, +the `ExecuteSwapOperations` message should be used instead, providing the route to follow for the swap to be executed +successfully. -After a swap takes place, the pool's balances are updated, and the fees are collected. The Bonding Manager receives the -protocol fee via the `FillRewards` message, while the swap fee remains in the pool to benefit the LP token holders, +After a swap takes place, the pool's balances are updated, and the fees are collected. The Bonding Manager receives the +protocol fee via the `FillRewards` message, while the swap fee remains in the pool to benefit the LP token holders, increasing the pool's liquidity and thus the LP token value. On Osmosis, there's an additional fee that is sent to the Osmosis community pool. diff --git a/contracts/liquidity_hub/vault-manager/README.md b/contracts/liquidity_hub/vault-manager/README.md index 0af25eaf..5d35f217 100644 --- a/contracts/liquidity_hub/vault-manager/README.md +++ b/contracts/liquidity_hub/vault-manager/README.md @@ -1,14 +1,14 @@ # Vault Manager -The Vault Manager is a contract handling a collection of single asset vaults that bots use to access flashloan capital for -arbitrage, liquidation, and other DeFi uses. By using the Flash Loan Vaults, arbitrage and liquidations happen locally -in a capital-efficient manner and without capital requirements. That is, each arbitrageur or liquidator will no longer -need their own capital on each local chain waiting idly to arb or to liquidate because they can access a flash loan for -their capital needs. When an arbitrage opportunity arises, an arbitrageur takes a flash loan, arbs the local dex price -versus a Migaloo pool, and then pays back the loan plus the flash loan fee. The arbitrageur then keeps the profit without -having used any of their own capital. - -Depositors of tokens into flash loan vaults benefit from fees paid when their vault is accessed for flash loans; the +The Vault Manager is a contract handling a collection of single asset vaults that bots use to access flashloan capital for +arbitrage, liquidation, and other DeFi uses. By using the Flash Loan Vaults, arbitrage and liquidations happen locally +in a capital-efficient manner and without capital requirements. That is, each arbitrageur or liquidator will no longer +need their own capital on each local chain waiting idly to arb or to liquidate because they can access a flash loan for +their capital needs. When an arbitrage opportunity arises, an arbitrageur takes a flash loan, arbs the local dex price +versus a Migaloo pool, and then pays back the loan plus the flash loan fee. The arbitrageur then keeps the profit without +having used any of their own capital. + +Depositors of tokens into flash loan vaults benefit from fees paid when their vault is accessed for flash loans; the greater the volume, the more fees generated. Flash loan vaults are a great source of yield with no impermanent loss. ## How it works @@ -29,32 +29,32 @@ to get the parameters right from the start. ### Deposits -Users can deposit into a vault at any time by calling the `Deposit` message, containing the vault identifier they wish to -deposit assets into. Each vault is stored in the state of the contract, and it keeps track of the assets deposited in it. +Users can deposit into a vault at any time by calling the `Deposit` message, containing the vault identifier they wish to +deposit assets into. Each vault is stored in the state of the contract, and it keeps track of the assets deposited in it. -When a user deposits into a vault, the assets are transferred to the vault's balance, while the user receives an LP token +When a user deposits into a vault, the assets are transferred to the vault's balance, while the user receives an LP token representing the share of the vault they own. The LP token can be used to withdraw the assets from the vault at any time. ### Withdrawals -Users can withdraw from a vault at any time by calling the `Withdraw` message, sending the amount of LP tokens they got -when depositing assets into a vault. Since each `Vault` has a unique LP token there's no need to specify a vault -identifier when withdrawing as the contract can find the Vault the user deposited into by analyzing the LP token sent. +Users can withdraw from a vault at any time by calling the `Withdraw` message, sending the amount of LP tokens they got +when depositing assets into a vault. Since each `Vault` has a unique LP token there's no need to specify a vault +identifier when withdrawing as the contract can find the Vault the user deposited into by analyzing the LP token sent. The LP tokens are burned and the original assets are transferred back to the user. ### Flash loans -Flash loans are a powerful tool that allows bots and users to borrow assets from the vault without any collateral, with -the condition that the assets plus the flash loan fees are returned within the same transaction, otherwise the transaction -is reverted as if nothing had happened. +Flash loans are a powerful tool that allows bots and users to borrow assets from the vault without any collateral, with +the condition that the assets plus the flash loan fees are returned within the same transaction, otherwise the transaction +is reverted as if nothing had happened. -A flash loan can be taken by calling the `FlashLoan` message, with the desired amount, vault identifier and the payload. -The payload is a list of messages that will be executed in the same transaction, and it doesn't need to include the "payback" +A flash loan can be taken by calling the `FlashLoan` message, with the desired amount, vault identifier and the payload. +The payload is a list of messages that will be executed in the same transaction, and it doesn't need to include the "payback" transaction as it is handled by the contract. -When a flash loan is taken, a boolean in the state is set to true on `ONGOING_FLASHLOAN`, so the funds can't be used to -be deposited back into a vault or to take another flash loan. After the payload is executed, the `CallbackMsg::AfterFlashloan` -is called. This makes sure the funds are back in the vault plus the fees. The profit made from the payload operations is -sent back to the originator of the flash loan. The Bonding Manager receives the protocol fees via the `FillRewards` message +When a flash loan is taken, a boolean in the state is set to true on `ONGOING_FLASHLOAN`, so the funds can't be used to +be deposited back into a vault or to take another flash loan. After the payload is executed, the `CallbackMsg::AfterFlashloan` +is called. This makes sure the funds are back in the vault plus the fees. The profit made from the payload operations is +sent back to the originator of the flash loan. The Bonding Manager receives the protocol fees via the `FillRewards` message and the users that have deposited into the vault where the flash loan was taken from receive the flash loan fees. diff --git a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs index 13bc8885..e8abb50f 100644 --- a/packages/white-whale-std/src/epoch_manager/epoch_manager.rs +++ b/packages/white-whale-std/src/epoch_manager/epoch_manager.rs @@ -8,21 +8,33 @@ use cw_controllers::HooksResponse; #[cw_serde] pub struct InstantiateMsg { + /// The initial epoch to start the contract with. pub start_epoch: Epoch, + /// The configuration for the epochs. pub epoch_config: EpochConfig, } #[cw_serde] pub enum ExecuteMsg { + /// Creates a new epoch. It's permissionless. A new epoch can only be created after the current + /// one has ended. CreateEpoch, + /// Adds a new hook to the hook registry, i.e. adds a contract to be notified when a new epoch + /// is created. AddHook { + /// The address of the contract to be added to the hook registry. contract_addr: String, }, + /// Removes a hook from the hook registry. RemoveHook { + /// The address of the contract to be removed from the hook registry. contract_addr: String, }, + /// Updates the contract configuration. UpdateConfig { + /// The new owner of the contract. owner: Option, + /// The new epoch configuration. epoch_config: Option, }, } @@ -32,58 +44,57 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// Returns the current epoch, which is the last on the EPOCHS map. #[returns(ConfigResponse)] - Config {}, - + Config, /// Returns the current epoch, which is the last on the EPOCHS map. #[returns(EpochResponse)] - CurrentEpoch {}, - + CurrentEpoch, /// Returns the epoch with the given id. #[returns(EpochResponse)] - Epoch { id: u64 }, - + Epoch { + /// The id of the epoch to be queried. + id: u64, + }, /// Returns the hooks in the registry. #[returns(HooksResponse)] - Hooks {}, - + Hooks, /// Returns whether or not a hook has been registered. #[returns(bool)] - Hook { hook: String }, + Hook { + /// The address of the contract to be checked. + hook: String, + }, } #[cw_serde] pub struct MigrateMsg {} +/// The epoch definition. #[cw_serde] -pub struct Config { - pub epoch_config: EpochConfig, +#[derive(Default)] +pub struct Epoch { + // Epoch identifier + pub id: u64, + // Epoch start time + pub start_time: Timestamp, } -impl Config { - pub fn to_config_response(self, owner: Addr) -> ConfigResponse { - ConfigResponse { - owner, - epoch_config: self.epoch_config, - } +impl Epoch { + pub fn to_epoch_response(self) -> EpochResponse { + EpochResponse { epoch: self } } } -#[cw_serde] -pub struct ConfigResponse { - pub owner: Addr, - pub epoch_config: EpochConfig, -} - -#[cw_serde] -pub struct EpochResponse { - pub epoch: Epoch, -} - -#[cw_serde] -pub struct ClaimableEpochsResponse { - pub epochs: Vec, +impl Display for Epoch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Epoch {{ id: {}, start_time: {} }}", + self.id, self.start_time, + ) + } } +/// The epoch configuration. #[cw_serde] pub struct EpochConfig { /// The duration of an epoch in nanoseconds. @@ -102,27 +113,34 @@ impl Display for EpochConfig { } } +/// The contract configuration. #[cw_serde] -#[derive(Default)] -pub struct Epoch { - // Epoch identifier - pub id: u64, - // Epoch start time - pub start_time: Timestamp, +pub struct Config { + /// The epoch configuration + pub epoch_config: EpochConfig, } -impl Epoch { - pub fn to_epoch_response(self) -> EpochResponse { - EpochResponse { epoch: self } +impl Config { + pub fn to_config_response(self, owner: Addr) -> ConfigResponse { + ConfigResponse { + owner, + epoch_config: self.epoch_config, + } } } -impl Display for Epoch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Epoch {{ id: {}, start_time: {} }}", - self.id, self.start_time, - ) - } +/// The response for the config query. +#[cw_serde] +pub struct ConfigResponse { + /// The owner of the contract. + pub owner: Addr, + /// The epoch configuration. + pub epoch_config: EpochConfig, +} + +/// The response for the current epoch query. +#[cw_serde] +pub struct EpochResponse { + /// The epoch queried. + pub epoch: Epoch, } From 0975046c534ac8cbc45dbe850abe7bd1bc59a5ec Mon Sep 17 00:00:00 2001 From: 0xFable <86927513+0xFable@users.noreply.github.com> Date: Fri, 24 May 2024 09:15:54 +0100 Subject: [PATCH 49/51] fix: Rework all v2 contracts to use bonding-manager not whale-lair (#363) * fix: Rework all v2 contracts to use bonding-manager not whale-lair + Removes all references to whale-lair for V2 contracts + Updates all schemas and most unit tests + Start of integration test rework for incentive manager The tests rework will take a bigger effort/another pass * fix: Update reference to whale_lair --- Cargo.lock | 2 + .../src/bin/bonding_manager_schema.rs | 2 +- .../incentive-manager/Cargo.toml | 2 + .../incentive-manager/src/contract.rs | 11 +- .../incentive-manager/src/helpers.rs | 2 +- .../incentive-manager/src/manager/commands.rs | 11 +- .../src/position/commands.rs | 6 +- .../incentive-manager/tests/common/suite.rs | 213 +++++++++++++++++- .../tests/common/suite_contracts.rs | 24 ++ .../incentive-manager/tests/integration.rs | 32 +-- .../pool-manager/schema/pool-manager.json | 14 +- .../pool-manager/schema/raw/execute.json | 14 +- .../pool-manager/src/contract.rs | 4 +- .../pool-manager/src/manager/commands.rs | 19 +- .../pool-manager/src/manager/update_config.rs | 8 +- .../pool-manager/src/swap/commands.rs | 2 +- .../pool-manager/src/tests/suite.rs | 8 +- .../vault-manager/src/contract.rs | 11 +- .../vault-manager/src/manager/commands.rs | 17 +- .../vault-manager/src/router/commands.rs | 4 +- .../vault-manager/tests/common/suite.rs | 16 +- .../vault-manager/tests/integration.rs | 4 +- .../white-whale-std/src/incentive_manager.rs | 6 +- packages/white-whale-std/src/pool_manager.rs | 4 +- packages/white-whale-std/src/vault_manager.rs | 10 +- 25 files changed, 339 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f89e05de..fe81c36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,6 +758,7 @@ name = "incentive-manager" version = "0.1.0" dependencies = [ "anyhow", + "bonding-manager", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", @@ -768,6 +769,7 @@ dependencies = [ "cw20", "cw20-base", "epoch-manager", + "pool-manager", "schemars", "semver", "serde", diff --git a/contracts/liquidity_hub/bonding-manager/src/bin/bonding_manager_schema.rs b/contracts/liquidity_hub/bonding-manager/src/bin/bonding_manager_schema.rs index cb2d7d75..6a50939b 100644 --- a/contracts/liquidity_hub/bonding-manager/src/bin/bonding_manager_schema.rs +++ b/contracts/liquidity_hub/bonding-manager/src/bin/bonding_manager_schema.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::write_api; -use white_whale_std::whale_lair::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use white_whale_std::bonding_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { write_api! { diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index e9ff898d..4b8b1640 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -43,3 +43,5 @@ white-whale-testing.workspace = true epoch-manager.workspace = true whale-lair.workspace = true anyhow.workspace = true +bonding-manager.workspace = true +pool-manager.workspace = true \ No newline at end of file diff --git a/contracts/liquidity_hub/incentive-manager/src/contract.rs b/contracts/liquidity_hub/incentive-manager/src/contract.rs index d27880f3..5dedd684 100644 --- a/contracts/liquidity_hub/incentive-manager/src/contract.rs +++ b/contracts/liquidity_hub/incentive-manager/src/contract.rs @@ -43,7 +43,7 @@ pub fn instantiate( let config = Config { epoch_manager_addr: deps.api.addr_validate(&msg.epoch_manager_addr)?, - whale_lair_addr: deps.api.addr_validate(&msg.bonding_manager_addr)?, + bonding_manager_addr: deps.api.addr_validate(&msg.bonding_manager_addr)?, create_incentive_fee: msg.create_incentive_fee, max_concurrent_incentives: msg.max_concurrent_incentives, max_incentive_epoch_buffer: msg.max_incentive_epoch_buffer, @@ -60,7 +60,10 @@ pub fn instantiate( ("action", "instantiate".to_string()), ("owner", msg.owner), ("epoch_manager_addr", config.epoch_manager_addr.to_string()), - ("whale_lair_addr", config.whale_lair_addr.to_string()), + ( + "bonding_manager_addr", + config.bonding_manager_addr.to_string(), + ), ("create_flow_fee", config.create_incentive_fee.to_string()), ( "max_concurrent_flows", @@ -134,7 +137,7 @@ pub fn execute( } }, ExecuteMsg::UpdateConfig { - whale_lair_addr, + bonding_manager_addr, epoch_manager_addr, create_incentive_fee, max_concurrent_incentives, @@ -147,7 +150,7 @@ pub fn execute( manager::commands::update_config( deps, info, - whale_lair_addr, + bonding_manager_addr, epoch_manager_addr, create_incentive_fee, max_concurrent_incentives, diff --git a/contracts/liquidity_hub/incentive-manager/src/helpers.rs b/contracts/liquidity_hub/incentive-manager/src/helpers.rs index 1f6dabe4..c5e72b29 100644 --- a/contracts/liquidity_hub/incentive-manager/src/helpers.rs +++ b/contracts/liquidity_hub/incentive-manager/src/helpers.rs @@ -72,7 +72,7 @@ pub(crate) fn process_incentive_creation_fee( // send incentive creation fee to whale lair for distribution messages.push(white_whale_std::bonding_manager::fill_rewards_msg( - config.whale_lair_addr.clone().into_string(), + config.bonding_manager_addr.clone().into_string(), vec![incentive_creation_fee.to_owned()], )?); diff --git a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs index 04c56883..6d4ba542 100644 --- a/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/manager/commands.rs @@ -355,7 +355,7 @@ pub(crate) fn on_epoch_changed( pub(crate) fn update_config( deps: DepsMut, info: MessageInfo, - whale_lair_addr: Option, + bonding_manager_addr: Option, epoch_manager_addr: Option, create_incentive_fee: Option, max_concurrent_incentives: Option, @@ -368,8 +368,8 @@ pub(crate) fn update_config( let mut config = CONFIG.load(deps.storage)?; - if let Some(whale_lair_addr) = whale_lair_addr { - config.whale_lair_addr = deps.api.addr_validate(&whale_lair_addr)?; + if let Some(new_bonding_manager_addr) = bonding_manager_addr { + config.bonding_manager_addr = deps.api.addr_validate(&new_bonding_manager_addr)?; } if let Some(epoch_manager_addr) = epoch_manager_addr { @@ -423,7 +423,10 @@ pub(crate) fn update_config( Ok(Response::default().add_attributes(vec![ ("action", "update_config".to_string()), - ("whale_lair_addr", config.whale_lair_addr.to_string()), + ( + "bonding_manager_addr", + config.bonding_manager_addr.to_string(), + ), ("epoch_manager_addr", config.epoch_manager_addr.to_string()), ("create_flow_fee", config.create_incentive_fee.to_string()), ( diff --git a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs index e668599b..5455fef3 100644 --- a/contracts/liquidity_hub/incentive-manager/src/position/commands.rs +++ b/contracts/liquidity_hub/incentive-manager/src/position/commands.rs @@ -242,11 +242,11 @@ pub(crate) fn withdraw_position( amount: penalty_fee, }; - let whale_lair_addr = CONFIG.load(deps.storage)?.whale_lair_addr; + let bonding_manager_addr = CONFIG.load(deps.storage)?.bonding_manager_addr; - // send penalty to whale lair for distribution + // send penalty to bonding manager for distribution messages.push(white_whale_std::bonding_manager::fill_rewards_msg( - whale_lair_addr.into_string(), + bonding_manager_addr.into_string(), vec![penalty], )?); diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs index f34290ce..39840cf7 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite.rs @@ -8,15 +8,19 @@ use cw_multi_test::{ use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig, EpochResponse}; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; +use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::incentive_manager::{ Config, IncentiveAction, IncentivesBy, IncentivesResponse, InstantiateMsg, LpWeightResponse, PositionAction, PositionsResponse, RewardsResponse, }; -use white_whale_std::pool_network::asset::AssetInfo; +use white_whale_std::pool_manager::PoolType; +use white_whale_std::pool_network::asset::{Asset, AssetInfo}; +use white_whale_testing::integration::contracts::whale_lair_contract; use white_whale_testing::multi_test::stargate_mock::StargateMock; use crate::common::suite_contracts::{ - epoch_manager_contract, incentive_manager_contract, whale_lair_contract, + bonding_manager_contract, epoch_manager_contract, incentive_manager_contract, + pool_manager_contract, }; type OsmosisTokenFactoryApp = App< @@ -36,7 +40,8 @@ pub struct TestingSuite { app: OsmosisTokenFactoryApp, pub senders: [Addr; 3], pub incentive_manager_addr: Addr, - pub whale_lair_addr: Addr, + pub bonding_manager_addr: Addr, + pub pool_manager_addr: Addr, pub epoch_manager_addr: Addr, pub pools: Vec, } @@ -103,7 +108,8 @@ impl TestingSuite { app, senders: [sender_1, sender_2, sender_3], incentive_manager_addr: Addr::unchecked(""), - whale_lair_addr: Addr::unchecked(""), + bonding_manager_addr: Addr::unchecked(""), + pool_manager_addr: Addr::unchecked(""), epoch_manager_addr: Addr::unchecked(""), pools: vec![], } @@ -111,8 +117,8 @@ impl TestingSuite { #[track_caller] pub(crate) fn instantiate_default(&mut self) -> &mut Self { - self.create_whale_lair(); self.create_epoch_manager(); + self.create_bonding_manager(); // April 4th 2024 15:00:00 UTC let timestamp = Timestamp::from_seconds(1712242800u64); @@ -120,7 +126,7 @@ impl TestingSuite { // instantiates the incentive manager contract self.instantiate( - self.whale_lair_addr.to_string(), + self.bonding_manager_addr.to_string(), self.epoch_manager_addr.to_string(), Coin { denom: "uwhale".to_string(), @@ -131,10 +137,54 @@ impl TestingSuite { 86_400, 31_536_000, Decimal::percent(10), //10% penalty - ) + ); + // self.create_pool_manager(); + // let empty_fee = Fee { + // share: Decimal::percent(0), + // }; + // let sender = self.senders[3].clone(); + + // self.app.wrap().query_all_balances(self.senders[3].clone()).unwrap(); + // println!("balances for {:?}: {:?}", self.senders[3], self.app.wrap().query_all_balances(self.senders[3].clone()).unwrap()); + // for each of ['osmo', 'lab'] make a pair against uwhale + // for asset in vec!["uosmo", "ulab"] { + // self.create_pair( + // sender.clone(), + // vec![asset.to_string(), "uwhale".to_string()], + // PoolFee { + // protocol_fee: empty_fee.clone(), + // swap_fee: empty_fee.clone(), + // burn_fee: empty_fee.clone(), + // extra_fees: vec![], + // }, + // PoolType::ConstantProduct, + // Some(format!("{}-uwhale", asset)), + // vec![], + // |res| { + // res.unwrap(); + // }, + // ); + // } + + // // For each of ['uosmo', 'ulab'] provide liquidity + // for asset in vec!["uosmo", "ulab"] { + // self.provide_liquidity( + // sender.clone(), + // format!("{}-uwhale", asset), + // vec![ + // coin(1_000_000_000, asset), + // coin(999998000/2, "uwhale"), + // ], + // |res| { + // res.unwrap(); + // }, + // ); + // } + + self } - fn create_whale_lair(&mut self) { + fn create_bonding_manager(&mut self) { let whale_lair_id = self.app.store_code(whale_lair_contract()); // create whale lair @@ -150,10 +200,21 @@ impl TestingSuite { }, ], }; + // let msg = white_whale_std::bonding_manager::InstantiateMsg { + // unbonding_period: Uint64::new(86400u64).u64(), + // growth_rate: Decimal::one(), + // bonding_assets: vec![ + // "bWHALE".to_string(), + // "ampWHALE".to_string(), + // ], + // distribution_denom: "uwhale".to_string(), + // grace_period: Uint64::new(21).u64(), + // epoch_manager_addr: self.epoch_manager_addr.to_string(), + // }; let creator = self.creator().clone(); - self.whale_lair_addr = self + self.bonding_manager_addr = self .app .instantiate_contract( whale_lair_id, @@ -197,6 +258,35 @@ impl TestingSuite { .unwrap(); } + #[allow(clippy::inconsistent_digit_grouping)] + fn create_pool_manager(&mut self) { + let pool_manager_contract = self.app.store_code(pool_manager_contract()); + + // create epoch manager + let msg = white_whale_std::pool_manager::InstantiateMsg { + bonding_manager_addr: self.bonding_manager_addr.to_string(), + incentive_manager_addr: self.incentive_manager_addr.to_string(), + pool_creation_fee: Coin { + denom: "uwhale".to_string(), + amount: Uint128::new(1000u128), + }, + }; + + let creator = self.creator().clone(); + + self.pool_manager_addr = self + .app + .instantiate_contract( + pool_manager_contract, + creator.clone(), + &msg, + &[], + "Pool Manager".to_string(), + Some(creator.to_string()), + ) + .unwrap(); + } + #[track_caller] #[allow(clippy::too_many_arguments)] pub(crate) fn instantiate( @@ -307,7 +397,7 @@ impl TestingSuite { pub(crate) fn update_config( &mut self, sender: Addr, - whale_lair_addr: Option, + bonding_manager_addr: Option, epoch_manager_addr: Option, create_incentive_fee: Option, max_concurrent_incentives: Option, @@ -319,7 +409,7 @@ impl TestingSuite { result: impl Fn(Result), ) -> &mut Self { let msg = white_whale_std::incentive_manager::ExecuteMsg::UpdateConfig { - whale_lair_addr, + bonding_manager_addr, epoch_manager_addr, create_incentive_fee, max_concurrent_incentives, @@ -600,3 +690,104 @@ impl TestingSuite { self } } + +impl TestingSuite { + #[track_caller] + pub(crate) fn provide_liquidity( + &mut self, + sender: Addr, + pool_identifier: String, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::pool_manager::ExecuteMsg::ProvideLiquidity { + pool_identifier, + slippage_tolerance: None, + receiver: None, + lock_position_identifier: None, + unlocking_duration: None, + max_spread: None, + }; + + result( + self.app + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &funds), + ); + + self + } + + #[track_caller] + pub(crate) fn swap( + &mut self, + sender: Addr, + _offer_asset: Coin, + ask_asset_denom: String, + belief_price: Option, + max_spread: Option, + receiver: Option, + pool_identifier: String, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::pool_manager::ExecuteMsg::Swap { + ask_asset_denom, + belief_price, + max_spread, + receiver, + pool_identifier, + }; + + result( + self.app + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &funds), + ); + + self + } + + #[track_caller] + pub(crate) fn add_swap_routes( + &mut self, + sender: Addr, + swap_routes: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::pool_manager::ExecuteMsg::AddSwapRoutes { swap_routes }; + + result( + self.app + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &[]), + ); + + self + } + #[track_caller] + pub(crate) fn create_pair( + &mut self, + sender: Addr, + asset_denoms: Vec, + pool_fees: PoolFee, + pool_type: PoolType, + pool_identifier: Option, + pair_creation_fee_funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::pool_manager::ExecuteMsg::CreatePool { + asset_denoms, + pool_fees, + pool_type, + pool_identifier, + asset_decimals: vec![6, 6], + }; + + result(self.app.execute_contract( + sender, + self.pool_manager_addr.clone(), + &msg, + &pair_creation_fee_funds, + )); + + self + } +} diff --git a/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs index cd419149..74293912 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/common/suite_contracts.rs @@ -25,6 +25,30 @@ pub fn whale_lair_contract() -> Box> { Box::new(contract) } +/// Creates the whale lair contract +pub fn bonding_manager_contract() -> Box> { + let contract = ContractWrapper::new( + bonding_manager::contract::execute, + bonding_manager::contract::instantiate, + bonding_manager::contract::query, + ) + .with_migrate(bonding_manager::contract::migrate); + + Box::new(contract) +} + +/// Creates the pool manager contract +pub fn pool_manager_contract() -> Box> { + let contract = ContractWrapper::new( + pool_manager::contract::execute, + pool_manager::contract::instantiate, + pool_manager::contract::query, + ) + .with_migrate(pool_manager::contract::migrate); + + Box::new(contract) +} + /// Creates the epoch manager contract pub fn epoch_manager_contract() -> Box> { let contract = ContractWrapper::new( diff --git a/contracts/liquidity_hub/incentive-manager/tests/integration.rs b/contracts/liquidity_hub/incentive-manager/tests/integration.rs index d3204a69..8791eee0 100644 --- a/contracts/liquidity_hub/incentive-manager/tests/integration.rs +++ b/contracts/liquidity_hub/incentive-manager/tests/integration.rs @@ -927,11 +927,11 @@ pub fn update_config() { suite.instantiate_default(); - let whale_lair = suite.whale_lair_addr.clone(); + let bonding_manager_addr = suite.bonding_manager_addr.clone(); let epoch_manager = suite.epoch_manager_addr.clone(); let expected_config = Config { - whale_lair_addr: whale_lair, + bonding_manager_addr, epoch_manager_addr: epoch_manager, create_incentive_fee: Coin { denom: "uwhale".to_string(), @@ -1094,7 +1094,7 @@ pub fn update_config() { ); let expected_config = Config { - whale_lair_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), + bonding_manager_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), epoch_manager_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), create_incentive_fee: Coin { denom: "uwhale".to_string(), @@ -1133,7 +1133,7 @@ pub fn test_manage_position() { suite.instantiate_default(); let incentive_manager = suite.incentive_manager_addr.clone(); - let whale_lair = suite.whale_lair_addr.clone(); + let whale_lair = suite.bonding_manager_addr.clone(); suite .add_hook(creator.clone(), incentive_manager, vec![], |result| { @@ -2136,9 +2136,9 @@ fn test_close_expired_incentives() { let lp_denom = "factory/pool/uLP".to_string(); let mut suite = TestingSuite::default_with_balances(vec![ - coin(1_000_000_000u128, "uwhale"), - coin(1_000_000_000u128, "ulab"), - coin(1_000_000_000u128, "uosmo"), + coin(2_000_000_000u128, "uwhale"), + coin(2_000_000_000u128, "ulab"), + coin(2_000_000_000u128, "uosmo"), coin(1_000_000_000u128, lp_denom.clone()), coin(1_000_000_000u128, "invalid_lp"), ]); @@ -2260,10 +2260,10 @@ fn expand_expired_incentive() { let lp_denom = "factory/pool/uLP".to_string(); let mut suite = TestingSuite::default_with_balances(vec![ - coin(1_000_000_000u128, "uwhale".to_string()), - coin(1_000_000_000u128, "ulab".to_string()), - coin(1_000_000_000u128, "uosmo".to_string()), - coin(1_000_000_000u128, lp_denom.clone()), + coin(2_000_000_000u128, "uwhale".to_string()), + coin(2_000_000_000u128, "ulab".to_string()), + coin(2_000_000_000u128, "uosmo".to_string()), + coin(2_000_000_000u128, lp_denom.clone()), ]); let creator = suite.creator(); @@ -2342,7 +2342,7 @@ fn test_emergency_withdrawal() { suite.instantiate_default(); - let whale_lair_addr = suite.whale_lair_addr.clone(); + let whale_lair_addr = suite.bonding_manager_addr.clone(); suite .manage_incentive( @@ -2446,7 +2446,7 @@ fn test_incentive_helper() { suite.instantiate_default(); let incentive_manager_addr = suite.incentive_manager_addr.clone(); - let whale_lair_addr = suite.whale_lair_addr.clone(); + let whale_lair_addr = suite.bonding_manager_addr.clone(); suite .manage_incentive( @@ -2590,7 +2590,7 @@ fn test_multiple_incentives_and_positions() { suite.instantiate_default(); let incentive_manager_addr = suite.incentive_manager_addr.clone(); - let whale_lair_addr = suite.whale_lair_addr.clone(); + let bonding_manager_addr = suite.bonding_manager_addr.clone(); // create 4 incentives with 2 different LPs suite @@ -2919,7 +2919,7 @@ fn test_multiple_incentives_and_positions() { ) .query_balance( lp_denom_1.clone().to_string(), - whale_lair_addr.clone(), + bonding_manager_addr.clone(), |balance| { // 10% of the lp the user input initially assert_eq!(balance, Uint128::new(4_000)); @@ -2927,7 +2927,7 @@ fn test_multiple_incentives_and_positions() { ) .query_balance( lp_denom_2.clone().to_string(), - whale_lair_addr.clone(), + bonding_manager_addr.clone(), |balance| { // 10% of the lp the user input initially assert_eq!(balance, Uint128::new(8_000)); diff --git a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json index b6932daf..a9bc00d7 100644 --- a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json +++ b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json @@ -379,6 +379,13 @@ "update_config": { "type": "object", "properties": { + "bonding_manager_addr": { + "description": "The new bonding-manager contract address.", + "type": [ + "string", + "null" + ] + }, "feature_toggle": { "description": "The new feature toggles of the contract, allowing fine-tuned control over which operations are allowed.", "anyOf": [ @@ -400,13 +407,6 @@ "type": "null" } ] - }, - "whale_lair_addr": { - "description": "The new whale-lair contract address.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json index e40859b6..2395931b 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json @@ -325,6 +325,13 @@ "update_config": { "type": "object", "properties": { + "bonding_manager_addr": { + "description": "The new bonding-manager contract address.", + "type": [ + "string", + "null" + ] + }, "feature_toggle": { "description": "The new feature toggles of the contract, allowing fine-tuned control over which operations are allowed.", "anyOf": [ @@ -346,13 +353,6 @@ "type": "null" } ] - }, - "whale_lair_addr": { - "description": "The new whale-lair contract address.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 0e8e4d03..bb5e7c61 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -175,13 +175,13 @@ pub fn execute( router::commands::remove_swap_routes(deps, info.sender, swap_routes) } ExecuteMsg::UpdateConfig { - whale_lair_addr, + bonding_manager_addr, pool_creation_fee, feature_toggle, } => manager::update_config( deps, info, - whale_lair_addr, + bonding_manager_addr, pool_creation_fee, feature_toggle, ), diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 0c679f11..bf7d2824 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -103,15 +103,16 @@ pub fn create_pool( // Prepare the sending of pool creation fee let mut messages: Vec = vec![]; - - // send pool creation fee to whale lair - let creation_fee = vec![config.pool_creation_fee]; - - // send pool creation fee to the bonding manager - messages.push(white_whale_std::bonding_manager::fill_rewards_msg( - config.bonding_manager_addr.into_string(), - creation_fee, - )?); + if !config.pool_creation_fee.amount.is_zero() { + // send pool creation fee to bonding manager + let creation_fee = vec![config.pool_creation_fee]; + + // send pool creation fee to the bonding manager + messages.push(white_whale_std::bonding_manager::fill_rewards_msg( + config.bonding_manager_addr.into_string(), + creation_fee, + )?); + } // Check if the asset infos are the same if asset_denoms diff --git a/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs b/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs index c0d90696..3fc11144 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/update_config.rs @@ -6,7 +6,7 @@ use crate::{state::CONFIG, ContractError}; pub fn update_config( deps: DepsMut, info: MessageInfo, - whale_lair_addr: Option, + bonding_manager_addr: Option, pool_creation_fee: Option, feature_toggle: Option, ) -> Result { @@ -14,9 +14,9 @@ pub fn update_config( cw_ownable::assert_owner(deps.storage, &info.sender)?; CONFIG.update(deps.storage, |mut config| { - if let Some(whale_lair_addr) = whale_lair_addr { - let whale_lair_addr = deps.api.addr_validate(&whale_lair_addr)?; - config.bonding_manager_addr = whale_lair_addr; + if let Some(new_bonding_manager_addr) = bonding_manager_addr { + let bonding_manager_addr = deps.api.addr_validate(&new_bonding_manager_addr)?; + config.bonding_manager_addr = bonding_manager_addr; } if let Some(pool_creation_fee) = pool_creation_fee { diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index a4bbeb53..b71116a4 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -33,7 +33,7 @@ pub fn swap( // verify that the assets sent match the ones from the pool let pool = get_pool_by_identifier(&deps.as_ref(), &pool_identifier)?; ensure!( - vec![ask_asset_denom.clone(), offer_asset.denom.clone()] + [ask_asset_denom.clone(), offer_asset.denom.clone()] .iter() .all(|asset| pool .assets diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 8c8aecaf..11880bb8 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -51,7 +51,7 @@ pub fn epoch_manager_contract() -> Box> { epoch_manager::contract::instantiate, epoch_manager::contract::query, ) - .with_migrate(whale_lair::contract::migrate); + .with_migrate(epoch_manager::contract::migrate); Box::new(contract) } @@ -63,7 +63,7 @@ pub fn incentive_manager_contract() -> Box> { incentive_manager::contract::instantiate, incentive_manager::contract::query, ) - .with_migrate(whale_lair::contract::migrate); + .with_migrate(incentive_manager::contract::migrate); Box::new(contract) } @@ -494,7 +494,7 @@ impl TestingSuite { pub(crate) fn update_config( &mut self, sender: Addr, - new_whale_lair_addr: Option, + new_bonding_manager_addr: Option, new_pool_creation_fee: Option, new_feature_toggle: Option, result: impl Fn(Result), @@ -503,7 +503,7 @@ impl TestingSuite { sender, self.pool_manager_addr.clone(), &white_whale_std::pool_manager::ExecuteMsg::UpdateConfig { - whale_lair_addr: new_whale_lair_addr.map(|addr| addr.to_string()), + bonding_manager_addr: new_bonding_manager_addr.map(|addr| addr.to_string()), pool_creation_fee: new_pool_creation_fee, feature_toggle: new_feature_toggle, }, diff --git a/contracts/liquidity_hub/vault-manager/src/contract.rs b/contracts/liquidity_hub/vault-manager/src/contract.rs index 48a724e0..24120e6b 100644 --- a/contracts/liquidity_hub/vault-manager/src/contract.rs +++ b/contracts/liquidity_hub/vault-manager/src/contract.rs @@ -25,7 +25,7 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let config = Config { - whale_lair_addr: deps.api.addr_validate(&msg.whale_lair_addr)?, + bonding_manager_addr: deps.api.addr_validate(&msg.bonding_manager_addr)?, vault_creation_fee: msg.vault_creation_fee.clone(), flash_loan_enabled: true, deposit_enabled: true, @@ -44,7 +44,10 @@ pub fn instantiate( Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), ("owner", msg.owner), - ("whale_lair_addr", config.whale_lair_addr.into_string()), + ( + "bonding_manager_addr", + config.bonding_manager_addr.into_string(), + ), ("vault_creation_fee", config.vault_creation_fee.to_string()), ])) } @@ -63,7 +66,7 @@ pub fn execute( vault_identifier, } => manager::commands::create_vault(deps, env, info, asset_denom, fees, vault_identifier), ExecuteMsg::UpdateConfig { - whale_lair_addr, + bonding_manager_addr, vault_creation_fee, flash_loan_enabled, deposit_enabled, @@ -71,7 +74,7 @@ pub fn execute( } => manager::commands::update_config( deps, info, - whale_lair_addr, + bonding_manager_addr, vault_creation_fee, flash_loan_enabled, deposit_enabled, diff --git a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs index fe9d1e1c..2774bc3f 100644 --- a/contracts/liquidity_hub/vault-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/vault-manager/src/manager/commands.rs @@ -32,15 +32,15 @@ pub fn create_vault( let mut messages: Vec = vec![]; - // send vault creation fee to whale lair + // send vault creation fee to bonding manager let creation_fee = coins( config.vault_creation_fee.amount.u128(), config.vault_creation_fee.denom.clone(), ); - // send protocol fee to whale lair + // send protocol fee to bonding manager messages.push(white_whale_std::bonding_manager::fill_rewards_msg( - config.whale_lair_addr.into_string(), + config.bonding_manager_addr.into_string(), creation_fee, )?); @@ -106,7 +106,7 @@ pub fn create_vault( pub fn update_config( deps: DepsMut, info: MessageInfo, - whale_lair_addr: Option, + bonding_manager_addr: Option, vault_creation_fee: Option, flash_loan_enabled: Option, deposit_enabled: Option, @@ -116,8 +116,8 @@ pub fn update_config( cw_ownable::assert_owner(deps.storage, &info.sender)?; let new_config = CONFIG.update::<_, ContractError>(deps.storage, |mut config| { - if let Some(new_whale_lair_addr) = whale_lair_addr { - config.whale_lair_addr = deps.api.addr_validate(&new_whale_lair_addr)?; + if let Some(new_bonding_manager_addr) = bonding_manager_addr { + config.bonding_manager_addr = deps.api.addr_validate(&new_bonding_manager_addr)?; } if let Some(vault_creation_fee) = vault_creation_fee { @@ -141,7 +141,10 @@ pub fn update_config( Ok(Response::default().add_attributes(vec![ ("method", "update_manager_config"), - ("whale_lair_addr", &new_config.whale_lair_addr.into_string()), + ( + "bonding_manager_addr", + &new_config.bonding_manager_addr.into_string(), + ), ( "vault_creation_fee", &new_config.vault_creation_fee.to_string(), diff --git a/contracts/liquidity_hub/vault-manager/src/router/commands.rs b/contracts/liquidity_hub/vault-manager/src/router/commands.rs index 448f1f14..9f167b7b 100644 --- a/contracts/liquidity_hub/vault-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/vault-manager/src/router/commands.rs @@ -212,9 +212,9 @@ pub fn after_flashloan( let config = CONFIG.load(deps.storage)?; - // send protocol fee to whale lair + // send protocol fee to bonding manager messages.push(white_whale_std::bonding_manager::fill_rewards_msg( - config.whale_lair_addr.into_string(), + config.bonding_manager_addr.into_string(), coins(protocol_fee.u128(), loan_asset.denom), )?); diff --git a/contracts/liquidity_hub/vault-manager/tests/common/suite.rs b/contracts/liquidity_hub/vault-manager/tests/common/suite.rs index f0219c00..fd3ecdaa 100644 --- a/contracts/liquidity_hub/vault-manager/tests/common/suite.rs +++ b/contracts/liquidity_hub/vault-manager/tests/common/suite.rs @@ -38,7 +38,7 @@ type OsmosisTokenFactoryApp = App< pub struct TestingSuite { app: OsmosisTokenFactoryApp, pub senders: [Addr; 3], - pub whale_lair_addr: Addr, + pub bonding_manager_addr: Addr, pub vault_manager_addr: Addr, pub cw20_tokens: Vec, pub pools: Vec, @@ -129,7 +129,7 @@ impl TestingSuite { Self { app, senders: [sender_1, sender_2, sender_3], - whale_lair_addr: Addr::unchecked(""), + bonding_manager_addr: Addr::unchecked(""), vault_manager_addr: Addr::unchecked(""), cw20_tokens: vec![], pools: vec![], @@ -146,7 +146,7 @@ impl TestingSuite { self.set_time(timestamp); self.instantiate( - self.whale_lair_addr.to_string(), + self.bonding_manager_addr.to_string(), Coin { denom: "uwhale".to_string(), amount: Uint128::new(1_000u128), @@ -173,14 +173,14 @@ impl TestingSuite { let creator = self.creator().clone(); - self.whale_lair_addr = self + self.bonding_manager_addr = self .app .instantiate_contract( whale_lair_id, creator.clone(), &msg, &[], - "White Whale Lair".to_string(), + "WW Bonding Manager".to_string(), Some(creator.to_string()), ) .unwrap(); @@ -253,12 +253,12 @@ impl TestingSuite { #[track_caller] pub(crate) fn instantiate( &mut self, - whale_lair_addr: String, + bonding_manager_addr: String, vault_creation_fee: Coin, ) -> &mut Self { let msg = InstantiateMsg { owner: self.creator().to_string(), - whale_lair_addr, + bonding_manager_addr, vault_creation_fee, }; @@ -373,7 +373,7 @@ impl TestingSuite { result: impl Fn(Result), ) -> &mut Self { let msg = white_whale_std::vault_manager::ExecuteMsg::UpdateConfig { - whale_lair_addr, + bonding_manager_addr: whale_lair_addr, vault_creation_fee, flash_loan_enabled, deposit_enabled, diff --git a/contracts/liquidity_hub/vault-manager/tests/integration.rs b/contracts/liquidity_hub/vault-manager/tests/integration.rs index ac505626..c924df8c 100644 --- a/contracts/liquidity_hub/vault-manager/tests/integration.rs +++ b/contracts/liquidity_hub/vault-manager/tests/integration.rs @@ -508,7 +508,7 @@ pub fn update_config() { let unauthorized = suite.senders[2].clone(); let initial_config = RefCell::new(white_whale_std::vault_manager::Config { - whale_lair_addr: Addr::unchecked(""), + bonding_manager_addr: Addr::unchecked(""), vault_creation_fee: Coin { denom: "uluna".to_string(), amount: Default::default(), @@ -559,7 +559,7 @@ pub fn update_config() { assert_eq!( new_config, white_whale_std::vault_manager::Config { - whale_lair_addr: Addr::unchecked( + bonding_manager_addr: Addr::unchecked( "migaloo1gqjwmexg70ajk439ckfjq0uw2k3u2qmqwy6axu" ), vault_creation_fee: Coin { diff --git a/packages/white-whale-std/src/incentive_manager.rs b/packages/white-whale-std/src/incentive_manager.rs index 9f3d5296..c07c421a 100644 --- a/packages/white-whale-std/src/incentive_manager.rs +++ b/packages/white-whale-std/src/incentive_manager.rs @@ -47,8 +47,8 @@ pub enum ExecuteMsg { Claim, /// Updates the config of the contract UpdateConfig { - /// The address to of the whale lair, to send fees to. - whale_lair_addr: Option, + /// The address to of the bonding manager, to send fees to. + bonding_manager_addr: Option, /// The epoch manager address, where the epochs are managed epoch_manager_addr: Option, /// The fee that must be paid to create an incentive. @@ -129,7 +129,7 @@ pub enum IncentivesBy { #[cw_serde] pub struct Config { /// The address to of the whale lair, to send fees to. - pub whale_lair_addr: Addr, + pub bonding_manager_addr: Addr, /// The epoch manager address, where the epochs are managed pub epoch_manager_addr: Addr, /// The fee that must be paid to create an incentive. diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index 19011b2c..c0188f68 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -258,8 +258,8 @@ pub enum ExecuteMsg { /// Updates the configuration of the contract. /// If a field is not specified (i.e., set to `None`), it will not be modified. UpdateConfig { - /// The new whale-lair contract address. - whale_lair_addr: Option, + /// The new bonding-manager contract address. + bonding_manager_addr: Option, /// The new fee that must be paid when a pool is created. pool_creation_fee: Option, /// The new feature toggles of the contract, allowing fine-tuned diff --git a/packages/white-whale-std/src/vault_manager.rs b/packages/white-whale-std/src/vault_manager.rs index e2b4849e..e2e141ae 100644 --- a/packages/white-whale-std/src/vault_manager.rs +++ b/packages/white-whale-std/src/vault_manager.rs @@ -9,8 +9,8 @@ use std::fmt::{Display, Formatter}; pub struct InstantiateMsg { /// The owner of the contract pub owner: String, - /// The whale lair address, where protocol fees are distributed - pub whale_lair_addr: String, + /// The bonding manager address, where protocol fees are distributed + pub bonding_manager_addr: String, /// The fee to create a vault pub vault_creation_fee: Coin, } @@ -18,8 +18,8 @@ pub struct InstantiateMsg { /// Configuration for the contract (manager) #[cw_serde] pub struct Config { - /// The whale lair contract address - pub whale_lair_addr: Addr, + /// The bonding manager contract address + pub bonding_manager_addr: Addr, /// The fee to create a new vault pub vault_creation_fee: Coin, /// If flash-loans are enabled @@ -92,7 +92,7 @@ pub enum ExecuteMsg { /// Updates the configuration of the vault manager. /// If a field is not specified, it will not be modified. UpdateConfig { - whale_lair_addr: Option, + bonding_manager_addr: Option, vault_creation_fee: Option, flash_loan_enabled: Option, deposit_enabled: Option, From 83026369030a685fc3bfabe60da0e3ff0ec6781e Mon Sep 17 00:00:00 2001 From: nahem Date: Fri, 31 May 2024 13:55:37 +0200 Subject: [PATCH 50/51] chore(repo): resolving dependency & package conflicts --- contracts/liquidity_hub/pool-manager/src/helpers.rs | 12 ++++++------ contracts/liquidity_hub/pool-manager/src/queries.rs | 4 +++- .../pool-network/stableswap_3pool/sim/Cargo.toml | 2 +- .../stableswap_3pool/src/stableswap_math/curve.rs | 2 +- xtask/src/main.rs | 1 + 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index e0b5902d..0c049568 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -493,8 +493,8 @@ pub struct OfferAmountComputation { pub fn assert_slippage_tolerance( slippage_tolerance: &Option, - deposits: &Vec, - pools: &Vec, + deposits: &[Coin], + pools: &[Coin], pool_type: PoolType, amount: Uint128, pool_token_supply: Uint128, @@ -695,7 +695,7 @@ pub fn get_asset_indexes_in_pool( // TODO: handle unwraps properly #[allow(clippy::unwrap_used)] -pub fn compute_d(amp_factor: &u64, deposits: &Vec) -> Option { +pub fn compute_d(amp_factor: &u64, deposits: &[Coin]) -> Option { let n_coins = Uint128::from(deposits.len() as u128); // sum(x_i), a.k.a S @@ -777,8 +777,8 @@ fn compute_next_d( #[allow(clippy::unwrap_used, clippy::too_many_arguments)] pub fn compute_mint_amount_for_deposit( amp_factor: &u64, - deposits: &Vec, - swaps: &Vec, + deposits: &[Coin], + swaps: &[Coin], pool_token_supply: Uint128, ) -> Option { // Initial invariant @@ -895,7 +895,7 @@ pub fn compute_y( no_swap: Uint128, d: Uint256, ) -> Option { - let amount = compute_y_raw(n_coins, &_factor, x, no_swap, d)?; + let amount = compute_y_raw(n_coins, amp_factor, x, no_swap, d)?; Some(Uint128::try_from(amount).unwrap()) } diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index ce272500..a02e4f3a 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -1,6 +1,8 @@ use std::cmp::Ordering; -use cosmwasm_std::{coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256}; +use cosmwasm_std::{ + coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256, +}; use white_whale_std::pool_manager::{ AssetDecimalsResponse, Config, PoolInfoResponse, PoolType, ReverseSimulationResponse, diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml index eb2b6968..25ba31e2 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Paul Stelzig paul@irulast.com>"] edition = "2021" [lib] -name = "sim" +name = "sim1" [dependencies] pyo3 = { version = "0.17.3", features = ["auto-initialize"] } diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs index 9249a55c..f4b8308b 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs @@ -394,7 +394,7 @@ mod tests { use super::*; use proptest::prelude::*; use rand::Rng; - use sim::Model; + use sim1::Model; use std::cmp; /// Timestamp at 0 diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ab04e749..dc4229c1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -97,6 +97,7 @@ pub mod tasks { member.name != "fee-distributor-mock" && member.name != "stableswap-3pool" && member.name != "stable-swap-sim" + && member.name != "stable-swap-sim1" }); for contract in contracts { From 750b02ab1c3e2ca7ed4d20946ad9e403c84f35af Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Tue, 4 Jun 2024 11:54:48 +0100 Subject: [PATCH 51/51] test: fix min liquidity amount check for stableswap --- .../incentive-manager/Cargo.toml | 2 +- .../pool-manager/src/liquidity/commands.rs | 18 +++--------- .../src/tests/integration_tests.rs | 28 ++++++++++++------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index 4b8b1640..9acc3d4b 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -44,4 +44,4 @@ epoch-manager.workspace = true whale-lair.workspace = true anyhow.workspace = true bonding-manager.workspace = true -pool-manager.workspace = true \ No newline at end of file +pool-manager.workspace = true diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 1a2c9252..6dea5bfc 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -221,10 +221,7 @@ pub fn provide_liquidity( .integer_sqrt() .as_u128(), ) - .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) - .map_err(|_| { - ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) - })?; + .saturating_sub(MINIMUM_LIQUIDITY_AMOUNT); // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT if share.is_zero() { @@ -268,18 +265,13 @@ pub fn provide_liquidity( if total_share == Uint128::zero() { // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool - let min_lp_token_amount = - MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(pool_assets.len() as u128); let share = Uint128::try_from(compute_d(amp_factor, &deposits).unwrap())? - .checked_sub(min_lp_token_amount) - .map_err(|_| { - ContractError::InvalidInitialLiquidityAmount(min_lp_token_amount) - })?; + .saturating_sub(MINIMUM_LIQUIDITY_AMOUNT); // share should be above zero after subtracting the min_lp_token_amount if share.is_zero() { return Err(ContractError::InvalidInitialLiquidityAmount( - min_lp_token_amount, + MINIMUM_LIQUIDITY_AMOUNT, )); } @@ -288,9 +280,7 @@ pub fn provide_liquidity( liquidity_token.clone(), &env.contract.address, &env.contract.address, - // TODO: check if the minimum liquidity amount is correct. - // min_lp_token_amount VS MINIMUM_LIQUIDITY_AMOUNT - min_lp_token_amount, + MINIMUM_LIQUIDITY_AMOUNT, )?); share diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index b4887d3a..8f3943f4 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -241,12 +241,14 @@ mod pool_creation_failures { |result| { let err = result.unwrap_err().downcast::().unwrap(); - match err { - ContractError::InvalidPoolCreationFee { .. } => {} - _ => panic!("Wrong error type, should return ContractError::InvalidPoolCreationFee"), - } - }, - ); + match err { + ContractError::InvalidPoolCreationFee { .. } => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidPoolCreationFee" + ), + } + }, + ); } #[test] @@ -3958,22 +3960,28 @@ mod provide_liquidity { vec![ Coin { denom: "uwhale".to_string(), - amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), }, Coin { denom: "uluna".to_string(), - amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), }, Coin { denom: "uusd".to_string(), - amount: Uint128::from(MINIMUM_LIQUIDITY_AMOUNT), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), }, ], |result| { assert_eq!( result.unwrap_err().downcast_ref::(), Some(&ContractError::InvalidInitialLiquidityAmount( - MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(3u128) + MINIMUM_LIQUIDITY_AMOUNT )) ); },